diff --git a/database/category.go b/database/category.go index af974e0..d0e28e4 100644 --- a/database/category.go +++ b/database/category.go @@ -1,6 +1,6 @@ /* * Amsterdam Web Communities System - * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -11,6 +11,7 @@ package database import ( "context" + "database/sql" "errors" "slices" "strings" @@ -64,19 +65,13 @@ func loadCategories(ctx context.Context) error { categoryMutex.Lock() defer categoryMutex.Unlock() if allCategories == nil { - rs, err := amdb.QueryContext(ctx, "SELECT COUNT(*) FROM refcategory") - if err != nil { - return err - } - if !rs.Next() { - return errors.New("internal error loading categories") - } + row := amdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM refcategory") var ncats int32 - if err = rs.Scan(&ncats); err != nil { + if err := row.Scan(&ncats); err != nil { return err } allCategories = make([]Category, 0, ncats) - if err = amdb.SelectContext(ctx, &allCategories, "SELECT * FROM refcategory ORDER BY parent, name"); err != nil { + if err := amdb.SelectContext(ctx, &allCategories, "SELECT * FROM refcategory ORDER BY parent, name"); err != nil { return err } for i, c := range allCategories { @@ -234,20 +229,15 @@ func AmSearchCategories(ctx context.Context, oper int, term string, offset int, queryString.WriteString(" AND hide_search = 0") } q := queryString.String() - rs, err := amdb.QueryContext(ctx, "SELECT COUNT(*) FROM refcategory WHERE "+q) - if err != nil { - return nil, -1, err - } - if !rs.Next() { - return nil, -1, errors.New("internal error getting category total") - } + row := amdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM refcategory WHERE "+q) var total int - if err = rs.Scan(&total); err != nil { + if err = row.Scan(&total); err != nil { return nil, total, err } if total == 0 { return make([]*Category, 0), 0, nil } + var rs *sql.Rows if offset > 0 { rs, err = amdb.QueryContext(ctx, "SELECT catid FROM refcategory WHERE "+q+" ORDER BY parent, name LIMIT ? OFFSET ?", max, offset) } else { diff --git a/database/community.go b/database/community.go index e65681b..d5decb2 100644 --- a/database/community.go +++ b/database/community.go @@ -1,6 +1,6 @@ /* * Amsterdam Web Communities System - * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -215,44 +215,35 @@ func (c *Community) Membership(ctx context.Context, u *User) (bool, bool, uint16 // "no join required" - they are effectively a member, but don't cache that return true, false, u.BaseLevel, nil } - rs, err := amdb.QueryContext(ctx, "SELECT locked, granted_lvl FROM commmember WHERE commid = ? AND uid = ?", c.Id, u.Uid) + row := amdb.QueryRowContext(ctx, "SELECT locked, granted_lvl FROM commmember WHERE commid = ? AND uid = ?", c.Id, u.Uid) + var locked bool + var level uint16 + err := row.Scan(&locked, &level) if err == nil { - if rs.Next() { - var locked bool - var level uint16 - if err = rs.Scan(&locked, &level); err == nil { - memberCache.Add(key, &memberCacheData{isMember: true, locked: locked, level: level}) - return true, locked, level, nil - } - } - if err == nil { - memberCache.Add(key, &memberCacheData{isMember: false, locked: false, level: uint16(0)}) - } + memberCache.Add(key, &memberCacheData{isMember: true, locked: locked, level: level}) + return true, locked, level, nil + } + if err == sql.ErrNoRows { + err = nil + memberCache.Add(key, &memberCacheData{isMember: false, locked: false, level: uint16(0)}) } return false, false, uint16(0), err } // MemberCount returns the number of members in the community. func (c *Community) MemberCount(ctx context.Context, hidden bool) (int, error) { - var rs *sql.Rows - var err error + var row *sql.Row if hidden { - rs, err = amdb.QueryContext(ctx, "SELECT COUNT(*) FROM commmember WHERE commid = ?", c.Id) + row = amdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM commmember WHERE commid = ?", c.Id) } else { - rs, err = amdb.QueryContext(ctx, "SELECT COUNT(*) FROM commmember WHERE commid = ? AND hidden = 0", c.Id) + row = amdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM commmember WHERE commid = ? AND hidden = 0", c.Id) } - if err != nil { + var rc int + if err := row.Scan(&rc); err == nil { + return rc, nil + } else { return -1, err } - if rs.Next() { - var rc int - if err = rs.Scan(&rc); err == nil { - return rc, nil - } else { - return -1, err - } - } - return -1, errors.New("internal error reading member count") } /* ListMembers lists or searches for community members matching certain criteria. @@ -314,16 +305,12 @@ func (c *Community) ListMembers(ctx context.Context, field int, oper int, term s query.WriteString(" AND m.hidden = 0") } q := query.String() - rs, err := amdb.QueryContext(ctx, `SELECT COUNT(*) FROM commmember m, users u, contacts c WHERE m.commid = ? AND m.uid = u.uid + row := amdb.QueryRowContext(ctx, `SELECT COUNT(*) FROM commmember m, users u, contacts c WHERE m.commid = ? AND m.uid = u.uid AND u.contactid = c.contactid`+q, c.Id) - if err != nil { - return nil, -1, err - } - if !rs.Next() { - return nil, -1, errors.New("internal error getting member count") - } var total int - if err = rs.Scan(&total); err == nil { + var err error + var rs *sql.Rows + if err = row.Scan(&total); err == nil { if offset > 0 { rs, err = amdb.QueryContext(ctx, `SELECT m.uid FROM commmember m, users u, contacts c WHERE m.commid = ? AND m.uid = u.uid AND u.contactid = c.contactid`+q+" ORDER BY u.username LIMIT ? OFFSET ?", c.Id, max, offset) @@ -381,35 +368,30 @@ func (c *Community) SetMembership(ctx context.Context, u *User, level uint16, lo } } } else { - rs, err := tx.QueryContext(ctx, "SELECT granted_lvl, locked FROM commmember WHERE commid = ? AND uid = ?", c.Id, u.Uid) + row := tx.QueryRowContext(ctx, "SELECT granted_lvl, locked FROM commmember WHERE commid = ? AND uid = ?", c.Id, u.Uid) + var oldLevel uint16 + var lockStatus bool + err := row.Scan(&oldLevel, &lockStatus) + switch err { + case nil: + if level != oldLevel || lockStatus != locked { + _, err = tx.ExecContext(ctx, "UPDATE commmember SET granted_lvl = ?, locked = ? WHERE commid = ? AND uid = ?", + level, locked, c.Id, u.Uid) + if err == nil { + stuffMembership(c.Id, u.Uid, true, locked, level) + } + } + case sql.ErrNoRows: + _, err = tx.ExecContext(ctx, "INSERT INTO commmember (commid, uid, granted_lvl, locked) VALUES (?, ?, ?, ?)", + c.Id, u.Uid, level, locked) + if err == nil { + stuffMembership(c.Id, u.Uid, true, locked, level) + err = AmOnUserJoinCommunityServices(ctx, tx, c, u) + } + } if err != nil { return err } - if rs.Next() { - var oldLevel uint16 - var lockStatus bool - if err = rs.Scan(&oldLevel, &lockStatus); err != nil { - return err - } - if level != oldLevel || lockStatus != locked { - _, err := tx.ExecContext(ctx, "UPDATE commmember SET granted_lvl = ?, locked = ? WHERE commid = ? AND uid = ?", - level, locked, c.Id, u.Uid) - if err != nil { - return err - } - stuffMembership(c.Id, u.Uid, true, locked, level) - } - } else { - _, err := tx.ExecContext(ctx, "INSERT INTO commmember (commid, uid, granted_lvl, locked) VALUES (?, ?, ?, ?)", - c.Id, u.Uid, level, locked) - if err != nil { - return err - } - stuffMembership(c.Id, u.Uid, true, locked, level) - if err = AmOnUserJoinCommunityServices(ctx, tx, c, u); err != nil { - return err - } - } } if err := c.TouchUpdateTx(ctx, tx); err == nil { ar := AmNewAudit(AuditCommunitySetMembership, personUID, ipaddr, fmt.Sprintf("cid=%d", c.Id), @@ -518,11 +500,8 @@ func (c *Community) SetProfileData(ctx context.Context, name string, alias strin c.CreateLevel = create_lvl c.DeleteLevel = delete_lvl c.JoinLevel = join_lvl - rs, err2 := amdb.QueryContext(ctx, "SELECT lastupdate FROM communities WHERE commid = ?", c.Id) - if err2 == nil { - rs.Next() - err2 = rs.Scan(&c.LastUpdate) - } + row := amdb.QueryRowContext(ctx, "SELECT lastupdate FROM communities WHERE commid = ?", c.Id) + err2 := row.Scan(&(c.LastUpdate)) if err2 != nil { log.Errorf("SetProfileData scan error: %v", err2) } @@ -547,13 +526,10 @@ func (c *Community) Touch(ctx context.Context) error { defer c.Mutex.Unlock() _, err := amdb.ExecContext(ctx, "UPDATE communities SET lastaccess = NOW() WHERE commid = ?", c.Id) if err == nil { - rs, err := amdb.QueryContext(ctx, "SELECT lastaccess FROM communities WHERE commid = ?", c.Id) - if err == nil { - rs.Next() - var na time.Time - if err = rs.Scan(&na); err == nil { - c.LastAccess = &na - } + row := amdb.QueryRowContext(ctx, "SELECT lastaccess FROM communities WHERE commid = ?", c.Id) + var na time.Time + if err = row.Scan(&na); err == nil { + c.LastAccess = &na } } return err @@ -565,14 +541,11 @@ func (c *Community) TouchUpdateTx(ctx context.Context, tx *sqlx.Tx) error { defer c.Mutex.Unlock() _, err := tx.ExecContext(ctx, "UPDATE communities SET lastaccess = NOW(), lastupdate = NOW() WHERE commid = ?", c.Id) if err == nil { - rs, err := tx.QueryContext(ctx, "SELECT lastaccess, lastupdate FROM communities WHERE commid = ?", c.Id) - if err != nil { - rs.Next() - var na, nu time.Time - if err = rs.Scan(&na, &nu); err == nil { - c.LastAccess = &na - c.LastUpdate = &nu - } + row := tx.QueryRowContext(ctx, "SELECT lastaccess, lastupdate FROM communities WHERE commid = ?", c.Id) + var na, nu time.Time + if err = row.Scan(&na, &nu); err == nil { + c.LastAccess = &na + c.LastUpdate = &nu } } return err @@ -657,16 +630,11 @@ func AmGetCommunityTx(ctx context.Context, tx *sqlx.Tx, id int32) (*Community, e * Standard Go error status (nil if community not found) */ func AmGetCommunityByAlias(ctx context.Context, alias string) (*Community, error) { - rs, err := amdb.QueryContext(ctx, "SELECT commid FROM communities WHERE alias = ?", alias) + row := amdb.QueryRowContext(ctx, "SELECT commid FROM communities WHERE alias = ?", alias) + var cid int32 + err := row.Scan(&cid) if err == nil { - if rs.Next() { - var cid int32 - if err = rs.Scan(&cid); err == nil { - return AmGetCommunity(ctx, cid) - } - } else { - return nil, nil - } + return AmGetCommunity(ctx, cid) } return nil, err } @@ -681,16 +649,11 @@ func AmGetCommunityByAlias(ctx context.Context, alias string) (*Community, error * Standard Go error status (nil if community not found) */ func AmGetCommunityByAliasTx(ctx context.Context, tx *sqlx.Tx, alias string) (*Community, error) { - rs, err := tx.QueryContext(ctx, "SELECT commid FROM communities WHERE alias = ?", alias) + row := tx.QueryRowContext(ctx, "SELECT commid FROM communities WHERE alias = ?", alias) + var cid int32 + err := row.Scan(&cid) if err == nil { - if rs.Next() { - var cid int32 - if err = rs.Scan(&cid); err == nil { - return AmGetCommunityTx(ctx, tx, cid) - } - } else { - return nil, nil - } + return AmGetCommunityTx(ctx, tx, cid) } return nil, err } @@ -916,13 +879,14 @@ func AmCreateCommunity(ctx context.Context, name string, alias string, hostUid i }() // validate alias does not already exist - rs, err := tx.QueryContext(ctx, "SELECT commid FROM communities WHERE alias = ?", alias) - if err != nil { + row := tx.QueryRowContext(ctx, "SELECT commid FROM communities WHERE alias = ?", alias) + err := row.Err() + if err != sql.ErrNoRows { + if err == nil { + err = errors.New("a community with that alias already exists") + } return nil, err } - if rs.Next() { - return nil, errors.New("a community with that alias already exists") - } // establish the community record _, err = tx.ExecContext(ctx, `INSERT INTO communities (createdate, lastaccess, lastupdate, read_lvl, write_lvl, @@ -982,24 +946,19 @@ func AmCreateCommunity(ctx context.Context, name string, alias string, hostUid i * Standard Go error status. */ func AmGetCommunitiesForCategory(ctx context.Context, catid int32, offset int, max int, showAll bool) ([]*Community, int, error) { - var rs *sql.Rows var err error + var row *sql.Row if showAll { - rs, err = amdb.QueryContext(ctx, "SELECT COUNT(*) FROM communities WHERE catid = ?", catid) + row = amdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM communities WHERE catid = ?", catid) } else { - rs, err = amdb.QueryContext(ctx, "SELECT COUNT(*) FROM communities WHERE catid = ? AND hide_dir = 0", catid) - } - if err != nil { - return nil, -1, err - } - if !rs.Next() { - return nil, -1, errors.New("internal error getting total match count") + row = amdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM communities WHERE catid = ? AND hide_dir = 0", catid) } var total int - err = rs.Scan(&total) + err = row.Scan(&total) if err != nil || total == 0 { return make([]*Community, 0), 0, err // short-circuit return } + var rs *sql.Rows if showAll { if offset > 0 { rs, err = amdb.QueryContext(ctx, "SELECT commid FROM communities WHERE catid = ? ORDER BY commname LIMIT ? OFFSET ?", @@ -1081,18 +1040,13 @@ func AmSearchCommunities(ctx context.Context, field int, oper int, term string, queryPortion.WriteString(" AND hide_search = 0") } q := queryPortion.String() - rs, err := amdb.QueryContext(ctx, "SELECT COUNT(*) FROM communities "+q) - if err != nil { - return nil, -1, err - } - if !rs.Next() { - return nil, -1, errors.New("internal error getting count") - } + row := amdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM communities "+q) var total int - err = rs.Scan(&total) + err := row.Scan(&total) if err != nil || total == 0 { return make([]*Community, 0), 0, err // short-circuit return } + var rs *sql.Rows if offset > 0 { rs, err = amdb.QueryContext(ctx, "SELECT commid FROM communities "+q+" ORDER BY commname LIMIT ? OFFSET ?", max, offset) } else { diff --git a/database/conference.go b/database/conference.go index df86724..20af1c0 100644 --- a/database/conference.go +++ b/database/conference.go @@ -1,6 +1,6 @@ /* * Amsterdam Web Communities System - * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -11,6 +11,7 @@ package database import ( "context" + "database/sql" "errors" "fmt" "sync" @@ -119,19 +120,16 @@ func (c *Conference) HostsQ(ctx context.Context) []*User { // Membership returns a membership flag and granted level for the user in this conference. func (c *Conference) Membership(ctx context.Context, u *User) (bool, uint16, error) { - rs, err := amdb.QueryContext(ctx, "SELECT granted_lvl FROM confmember WHERE confid = ? AND uid = ?", c.ConfId, u.Uid) - if err != nil { - return false, 0, err + row := amdb.QueryRowContext(ctx, "SELECT granted_lvl FROM confmember WHERE confid = ? AND uid = ?", c.ConfId, u.Uid) + var level uint16 + err := row.Scan(&level) + switch err { + case nil: + return true, level, nil + case sql.ErrNoRows: + return false, 0, nil } - rc := false - if rs.Next() { - rc = true - var level uint16 - if err = rs.Scan(&level); err == nil { - return rc, level, nil - } - } - return rc, 0, err + return false, 0, err } /* TestPermission is shorthand that tests if a user has a permission with respect to the conference. @@ -306,14 +304,11 @@ func AmGetConferenceByAlias(ctx context.Context, alias string) (*Conference, err if ok { confid = xconf.(int32) } else { - rs, err := amdb.QueryContext(ctx, "SELECT confid FROM confalias WHERE alias = ?", alias) - if err != nil { - return nil, err - } - if !rs.Next() { + row := amdb.QueryRowContext(ctx, "SELECT confid FROM confalias WHERE alias = ?", alias) + err := row.Scan(&confid) + if err == sql.ErrNoRows { return nil, fmt.Errorf("alias not found: %s", alias) - } - if err = rs.Scan(&confid); err != nil { + } else if err != nil { return nil, err } conferenceAliasMap.Store(alias, confid) @@ -331,19 +326,17 @@ func AmGetConferenceByAlias(ctx context.Context, alias string) (*Conference, err * Standard Go error status. */ func AmGetConferenceByAliasInCommunity(ctx context.Context, cid int32, alias string) (*Conference, error) { - rs, err := amdb.QueryContext(ctx, `SELECT c.confid FROM commtoconf c, confalias a WHERE c.confid = a.confid + row := amdb.QueryRowContext(ctx, `SELECT c.confid FROM commtoconf c, confalias a WHERE c.confid = a.confid AND c.commid = ? AND a.alias = ?`, cid, alias) - if err != nil { - return nil, err - } - if !rs.Next() { + var confid int32 + err := row.Scan(&confid) + switch err { + case nil: + AmGetConference(ctx, confid) + case sql.ErrNoRows: return nil, errors.New("conference not found") } - var confid int32 - if err = rs.Scan(&confid); err != nil { - return nil, err - } - return AmGetConference(ctx, confid) + return nil, err } /* AmGetCommunityConferences returns all conferences for a given community. diff --git a/database/contactinfo.go b/database/contactinfo.go index 1cd3438..b4f1e69 100644 --- a/database/contactinfo.go +++ b/database/contactinfo.go @@ -1,6 +1,6 @@ /* * Amsterdam Web Communities System - * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -11,7 +11,7 @@ package database import ( "context" - "errors" + "database/sql" "fmt" "strings" "sync" @@ -54,24 +54,16 @@ type ContactInfo struct { // lookupCommunityContact looks up the ID of a contact for a community. func lookupCommunityContact(ctx context.Context, id int32) (int32, error) { var rc int32 = -1 - rs, err := amdb.QueryContext(ctx, "SELECT contactid FROM contacts WHERE owner_commid = ?", id) - if err == nil { - if rs.Next() { - err = rs.Scan(&rc) - } - } + row := amdb.QueryRowContext(ctx, "SELECT contactid FROM contacts WHERE owner_commid = ?", id) + err := row.Scan(&rc) return rc, err } // lookupUserContact looks up the ID of a contact for a user. func lookupUserContact(ctx context.Context, uid int32) (int32, error) { var rc int32 = -1 - rs, err := amdb.QueryContext(ctx, "SELECT contactid FROM contacts WHERE owner_uid = ? AND owner_commid = -1", uid) - if err == nil { - if rs.Next() { - err = rs.Scan(&rc) - } - } + row := amdb.QueryRowContext(ctx, "SELECT contactid FROM contacts WHERE owner_uid = ? AND owner_commid = -1", uid) + err := row.Scan(&rc) return rc, err } @@ -150,12 +142,12 @@ func (ci *ContactInfo) Save(ctx context.Context) (bool, error) { } if !emailChange { // we don't THINK the E-mail address is changing, but we could be wrong... - rs, err := amdb.QueryContext(ctx, "SELECT contactid FROM contacts WHERE contactid = ? AND email = ?", ci.ContactId, ci.Email) - if err != nil { - return false, err - } - if !rs.Next() { + row := amdb.QueryRowContext(ctx, "SELECT contactid FROM contacts WHERE contactid = ? AND email = ?", ci.ContactId, ci.Email) + err := row.Err() + if err == sql.ErrNoRows { emailChange = true + } else if err != nil { + return false, err } } // Handle the database heavy lifting. @@ -184,14 +176,11 @@ func (ci *ContactInfo) Save(ctx context.Context) (bool, error) { contactCache.Add(ci.ContactId, ci) } // Refresh the last update date. - rs, err := amdb.QueryContext(ctx, "SELECT lastupdate FROM contacts WHERE contactid = ?", ci.ContactId) + row := amdb.QueryRowContext(ctx, "SELECT lastupdate FROM contacts WHERE contactid = ?", ci.ContactId) + err := row.Scan(&(ci.LastUpdate)) if err != nil { return false, err } - if !rs.Next() { - return false, errors.New("internal error rereading update timestamp") - } - err = rs.Scan(&ci.LastUpdate) return emailChange, err } diff --git a/database/emailban.go b/database/emailban.go index 909ceca..b013748 100644 --- a/database/emailban.go +++ b/database/emailban.go @@ -1,6 +1,6 @@ /* * Amsterdam Web Communities System - * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -9,7 +9,10 @@ // The database package contains database management and storage logic. package database -import "context" +import ( + "context" + "database/sql" +) /* AmIsEmailAddressBanned returns true if the given E-mail address is on the "banned" list. * Parameters: @@ -20,9 +23,12 @@ import "context" * Standard Go error status. */ func AmIsEmailAddressBanned(ctx context.Context, address string) (bool, error) { - rs, err := amdb.QueryContext(ctx, "SELECT by_uid FROM emailban WHERE address = ?", address) - if err != nil { - return false, err + row := amdb.QueryRowContext(ctx, "SELECT by_uid FROM emailban WHERE address = ?", address) + switch row.Err() { + case nil: + return true, nil + case sql.ErrNoRows: + return false, nil } - return rs.Next(), nil + return false, row.Err() } diff --git a/database/globals.go b/database/globals.go index cfded6e..bda9f1d 100644 --- a/database/globals.go +++ b/database/globals.go @@ -1,6 +1,6 @@ /* * Amsterdam Web Communities System - * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -11,6 +11,7 @@ package database import ( "context" + "database/sql" "errors" "sync" @@ -115,23 +116,20 @@ func AmGlobals(ctx context.Context) (*Globals, error) { func AmGetGlobalProperty(ctx context.Context, index int32) (string, error) { globalPropMutex.Lock() defer globalPropMutex.Unlock() + var err error = nil rc, ok := globalProps[index] if !ok { - rs, err := amdb.QueryContext(ctx, "SELECT data FROM propglobal WHERE ndx = ?", index) - if err != nil { - return "", err - } - if rs.Next() { - err = rs.Scan(&rc) - if err != nil { - return "", err - } + row := amdb.QueryRowContext(ctx, "SELECT data FROM propglobal WHERE ndx = ?", index) + err = row.Scan(&rc) + switch err { + case nil: globalProps[index] = rc - return rc, nil + case sql.ErrNoRows: + rc = "" + err = nil } - rc = "" } - return rc, nil + return rc, err } /* AmSetGlobalProperty sets the value of a global property. @@ -147,11 +145,15 @@ func AmSetGlobalProperty(ctx context.Context, index int32, value string) error { defer globalPropMutex.Unlock() _, updateMode := globalProps[index] if !updateMode { - rs, err := amdb.QueryContext(ctx, "SELECT data FROM propglobal WHERE ndx = ?", index) - if err != nil { - return err + row := amdb.QueryRowContext(ctx, "SELECT data FROM propglobal WHERE ndx = ?", index) + switch row.Err() { + case nil: + updateMode = true + case sql.ErrNoRows: + updateMode = false + default: + return row.Err() } - updateMode = rs.Next() } var err error = nil if updateMode { diff --git a/database/imagestore.go b/database/imagestore.go index 56f143d..21b9b68 100644 --- a/database/imagestore.go +++ b/database/imagestore.go @@ -1,6 +1,6 @@ /* * Amsterdam Web Communities System - * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -86,17 +86,12 @@ func AmLoadImage(ctx context.Context, id int32) (*ImageStore, error) { * Standard Go error status. */ func AmStoreImage(ctx context.Context, typecode int16, owner int32, mimetype string, data []byte) (*ImageStore, error) { - rs, err := amdb.QueryContext(ctx, "SELECT imgid FROM imagestore WHERE typecode = ? AND ownerid = ?", typecode, owner) - if err != nil { - return nil, err - } var img *ImageStore - if rs.Next() { - var id int32 - err = rs.Scan(&id) - if err != nil { - return nil, err - } + row := amdb.QueryRowContext(ctx, "SELECT imgid FROM imagestore WHERE typecode = ? AND ownerid = ?", typecode, owner) + var id int32 + err := row.Scan(&id) + switch err { + case nil: img, err = AmLoadImage(ctx, id) if err != nil { return nil, err @@ -104,7 +99,7 @@ func AmStoreImage(ctx context.Context, typecode int16, owner int32, mimetype str img.MimeType = mimetype img.Length = int32(len(data)) img.Data = data - } else { + case sql.ErrNoRows: img = &ImageStore{ ImgId: -1, TypeCode: typecode, @@ -113,6 +108,8 @@ func AmStoreImage(ctx context.Context, typecode int16, owner int32, mimetype str Length: int32(len(data)), Data: data, } + default: + return nil, err } err = img.Save(ctx) if err != nil { diff --git a/database/ipban.go b/database/ipban.go index 4dcf4ba..4c3598e 100644 --- a/database/ipban.go +++ b/database/ipban.go @@ -1,6 +1,6 @@ /* * Amsterdam Web Communities System - * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -11,6 +11,7 @@ package database import ( "context" + "database/sql" "fmt" "math/big" "net" @@ -65,21 +66,17 @@ func AmTestIPBan(ctx context.Context, ip_address string) (string, error) { iv.SetBytes(addr) iv_lo := big.NewInt(0).And(iv, low64mask).Uint64() iv_hi := big.NewInt(0).Rsh(iv, 64).Uint64() - rows, err := amdb.QueryContext(ctx, `SELECT message FROM ipban WHERE (address_lo & mask_lo) = (? & mask_lo) + row := amdb.QueryRowContext(ctx, `SELECT message FROM ipban WHERE (address_lo & mask_lo) = (? & mask_lo) AND (address_hi & mask_hi) = (? & mask_hi) AND (expire IS NULL OR expire >= ?) AND enable <> 0 ORDER BY mask_hi DESC, mask_lo DESC`, iv_lo, iv_hi, time.Now().UTC()) - if err != nil { - return "", err - } - defer rows.Close() - if rows.Next() { - err = rows.Scan(&rc) - if err != nil { - return "", err - } + err := row.Scan(&rc) + switch err { + case nil: knownBans[ip_address] = rc return rc, nil + case sql.ErrNoRows: + knownGood[ip_address] = true + return "", nil } - knownGood[ip_address] = true - return "", nil + return "", err } diff --git a/database/post.go b/database/post.go index c039ced..edec40a 100644 --- a/database/post.go +++ b/database/post.go @@ -13,6 +13,7 @@ import ( "bytes" "compress/gzip" "context" + "database/sql" "errors" "fmt" "strings" @@ -63,15 +64,9 @@ func (p *PostHeader) IsScribbled() bool { // IsPublished returns true if the post has been published to the front page. func (p *PostHeader) IsPublished(ctx context.Context) (bool, error) { - rs, err := amdb.QueryContext(ctx, "SELECT COUNT(*) FROM postpublish WHERE postid = ?", p.PostId) - if err != nil { - return false, err - } - if !rs.Next() { - return false, errors.New("internal failure in IsPublished") - } + row := amdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM postpublish WHERE postid = ?", p.PostId) ct := 0 - err = rs.Scan(&ct) + err := row.Scan(&ct) return ct > 0, err } @@ -86,16 +81,16 @@ func (p *PostHeader) AttachmentInfo(ctx context.Context) (*PostAttachInfo, error if p.ScribbleDate != nil && p.ScribbleUid != nil { return nil, errors.New("no attachment data for scribbled post") } - rs, err := amdb.QueryContext(ctx, "SELECT filename, mimetype, datalen FROM postattach WHERE postid = ?", p.PostId) - if err != nil { - return nil, err - } - if !rs.Next() { + row := amdb.QueryRowContext(ctx, "SELECT filename, mimetype, datalen FROM postattach WHERE postid = ?", p.PostId) + var rc PostAttachInfo + err := row.Scan(&(rc.Filename), &(rc.MIMEType), &(rc.Length)) + switch err { + case nil: + return &rc, nil + case sql.ErrNoRows: return nil, nil } - var rc PostAttachInfo - err = rs.Scan(&(rc.Filename), &(rc.MIMEType), &(rc.Length)) - return &rc, err + return nil, err } /* AttachmentData returns attachment data for a post. @@ -109,18 +104,14 @@ func (p *PostHeader) AttachmentData(ctx context.Context) ([]byte, error) { if p.ScribbleDate != nil && p.ScribbleUid != nil { return nil, errors.New("no attachment data for scribbled post") } - rs, err := amdb.QueryContext(ctx, "SELECT datalen, stgmethod, data FROM postattach WHERE postid = ?", p.PostId) - if err != nil { - return nil, err - } - if !rs.Next() { - return nil, nil - } + row := amdb.QueryRowContext(ctx, "SELECT datalen, stgmethod, data FROM postattach WHERE postid = ?", p.PostId) var datalen int32 var stgmethod int16 var dbdata []byte - err = rs.Scan(&datalen, &stgmethod, &dbdata) - if err != nil { + err := row.Scan(&datalen, &stgmethod, &dbdata) + if err == sql.ErrNoRows { + return nil, nil + } else if err != nil { return nil, err } if stgmethod == stgMethodPlain { @@ -281,15 +272,9 @@ func (p *PostHeader) Scribble(ctx context.Context, u *User, ipaddr string) error } // Reread the scribble date. - rs, err := tx.QueryContext(ctx, "SELECT scribble_date FROM posts WHERE postid = ?", p.PostId) - if err != nil { - return err - } - if !rs.Next() { - return errors.New("internal error while scribbling") - } + row := tx.QueryRowContext(ctx, "SELECT scribble_date FROM posts WHERE postid = ?", p.PostId) var newScribbleDate time.Time - if err = rs.Scan(&newScribbleDate); err != nil { + if err = row.Scan(&newScribbleDate); err != nil { return err } diff --git a/database/topic.go b/database/topic.go index bc49df9..8507fe7 100644 --- a/database/topic.go +++ b/database/topic.go @@ -56,14 +56,9 @@ func (t *Topic) GetPost(ctx context.Context, num int32) (*PostHeader, error) { // GetLastRead returns the "last read" message for a user on a topic. func (t *Topic) GetLastRead(ctx context.Context, u *User) (int32, error) { - rs, err := amdb.QueryContext(ctx, "SELECT last_message FROM topicsettings WHERE topicid = ? AND uid = ?", t.TopicId, u.Uid) - if err != nil { - return -1, err - } + row := amdb.QueryRowContext(ctx, "SELECT last_message FROM topicsettings WHERE topicid = ? AND uid = ?", t.TopicId, u.Uid) var rc int32 = -1 - if rs.Next() { - err = rs.Scan(&rc) - } + err := row.Scan(&rc) return rc, err } @@ -83,14 +78,9 @@ func (t *Topic) SetLastRead(ctx context.Context, u *User, postNum int32) error { // IsHidden tells us whether the user has the topic hidden. func (t *Topic) IsHidden(ctx context.Context, u *User) (bool, error) { - rs, err := amdb.QueryContext(ctx, "SELECT hidden FROM topicsettings WHERE topicid = ? AND uid = ?", t.TopicId, u.Uid) - if err != nil { - return false, err - } + row := amdb.QueryRowContext(ctx, "SELECT hidden FROM topicsettings WHERE topicid = ? AND uid = ?", t.TopicId, u.Uid) rc := false - if rs.Next() { - err = rs.Scan(&rc) - } + err := row.Scan(&rc) return rc, err } diff --git a/database/user.go b/database/user.go index 2e4b682..dc2edd4 100644 --- a/database/user.go +++ b/database/user.go @@ -1,6 +1,6 @@ /* * Amsterdam Web Communities System - * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -12,6 +12,7 @@ package database import ( "context" "crypto/sha1" + "database/sql" "encoding/hex" "errors" "fmt" @@ -465,18 +466,8 @@ func AmGetUserByName(ctx context.Context, name string, tx *sqlx.Tx) (*User, erro // getAnonUserID retrieves the UID of the "anonymous" user from the database. func getAnonUserID(ctx context.Context) (int32, error) { if anonUid < 0 { - rows, err := amdb.QueryContext(ctx, "SELECT uid FROM users WHERE is_anon = 1") - if err == nil { - defer rows.Close() - if rows.Next() { - err = rows.Scan(&anonUid) - if err == nil && rows.Next() { - err = fmt.Errorf("should be only one anonymous user in Amsterdam database") - } - } else { - err = fmt.Errorf("no anonymous user in Amsterdam database") - } - } + row := amdb.QueryRowContext(ctx, "SELECT uid FROM users WHERE is_anon = 1") + err := row.Scan(&anonUid) if err != nil { return -1, err } @@ -706,12 +697,12 @@ func AmCreateNewUser(ctx context.Context, username string, password string, remi }() // Test if the user name is already taken. - rs, err := tx.QueryContext(ctx, "SELECT uid FROM users WHERE username = ?", username) - if err != nil { - return nil, err - } else if rs.Next() { + row := tx.QueryRowContext(ctx, "SELECT uid FROM users WHERE username = ?", username) + if row.Err() == nil { log.Warnf("username \"%s\" already exists", username) return nil, errors.New("that user name already exists. Please try again") + } else if row.Err() != sql.ErrNoRows { + return nil, row.Err() } // Insert the user record. @@ -730,7 +721,7 @@ func AmCreateNewUser(ctx context.Context, username string, password string, remi log.Debugf("...created new user \"%s\" with UID %d", username, user.Uid) // add user preferences - _, err = tx.ExecContext(ctx, "INSERT INTO userprefs (uid) VALUES (?)", user.Uid) + _, err := tx.ExecContext(ctx, "INSERT INTO userprefs (uid) VALUES (?)", user.Uid) if err != nil { return nil, err } @@ -895,20 +886,16 @@ func AmSearchUsers(ctx context.Context, field int, oper int, term string, offset return nil, -1, errors.New("invalid operator selector") } q := queryPortion.String() - rs, err := amdb.QueryContext(ctx, "SELECT COUNT(*) FROM users u, contacts c WHERE u.contactid = c.contactid AND u.is_anon = 0 AND "+q) - if err != nil { - return nil, -1, err - } - if !rs.Next() { - return nil, -1, errors.New("internal error getting count") - } + row := amdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM users u, contacts c WHERE u.contactid = c.contactid AND u.is_anon = 0 AND "+q) var total int - if err = rs.Scan(&total); err != nil { + err := row.Scan(&total) + if err != nil { return nil, -1, err } if total == 0 { return make([]*User, 0), 0, nil } + var rs *sql.Rows if offset > 0 { rs, err = amdb.QueryContext(ctx, "SELECT u.uid FROM users u, contacts c WHERE u.contactid = c.contactid AND u.is_anon = 0 AND "+q+ " ORDER BY u.username LIMIT ? OFFSET ?", max, offset)