# HG changeset patch
# User Christophe de Vienne <christophe@cdevienne.info>
# Date 1736518261 -3600
#      Fri Jan 10 15:11:01 2025 +0100
# Node ID 42db5ca3dedd60721287f4a698b6238b155be074
# Parent  5d141f571cf00f4311f863f23dbf9533c7f43318
sql: add a 'Row' api

diff --git a/database/sql.go b/database/sql.go
--- a/database/sql.go
+++ b/database/sql.go
@@ -57,6 +57,7 @@
 	GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
 	SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
 	QueryxContext(context.Context, string, ...interface{}) (*sqlx.Rows, error)
+	QueryRowxContext(context.Context, string, ...interface{}) *sqlx.Row
 }
 
 func setPlaceHolderFormat(query squirrel.Sqlizer) squirrel.Sqlizer {
@@ -135,6 +136,19 @@
 	return e.QueryxContext(ctx, sqlQuery, args...)
 }
 
+// QueryRowContext runs a squirrel query on the given db or tx and return a single Row.
+func QueryRowContext(ctx context.Context, e SQLExecutor, query squirrel.Sqlizer, log *zerolog.Logger) *Row {
+	query = setPlaceHolderFormat(query)
+	sqlQuery, args, err := query.
+		ToSql()
+	if err != nil {
+		return &Row{err: err}
+	}
+	SQLTrace(log, sqlQuery, args)
+
+	return &Row{Row: e.QueryRowxContext(ctx, sqlQuery, args...)}
+}
+
 // ValuesMap returns the values for a list of columns as a map. If a column does
 // not exits, the corresponding value is set to nil.
 func ValuesMap(m Mapped, columns ...string) map[string]interface{} {
diff --git a/database/sql_helper.go b/database/sql_helper.go
--- a/database/sql_helper.go
+++ b/database/sql_helper.go
@@ -123,6 +123,11 @@
 	return h.sqle.QueryxContext(h.ctx, sql, args...)
 }
 
+// QueryRow executes a query that returns a single row.
+func (h *SQLHelper) QueryRow(query sq.Sqlizer) *Row {
+	return QueryRowContext(h.ctx, h.sqle, query, &h.log)
+}
+
 // Insert inserts a Mapped into the db.
 func (h *SQLHelper) Insert(instances ...Mapped) (sql.Result, error) {
 	query := SQLInsert(instances...)
diff --git a/database/sql_row.go b/database/sql_row.go
new file mode 100644
--- /dev/null
+++ b/database/sql_row.go
@@ -0,0 +1,44 @@
+package database
+
+import (
+	"database/sql"
+
+	"github.com/jmoiron/sqlx"
+)
+
+type Row struct {
+	*sqlx.Row
+	err error
+}
+
+// Scan is a fixed implementation of sql.Row.Scan, which does not discard the
+// underlying error from the internal rows object if it exists.
+func (r *Row) Scan(dest ...interface{}) error {
+	if r.err != nil {
+		return r.err
+	}
+
+	return r.Row.Scan(dest...)
+}
+
+// Columns returns the underlying sql.Rows.Columns(), or the deferred error usually
+// returned by Row.Scan()
+func (r *Row) Columns() ([]string, error) {
+	if r.err != nil {
+		return []string{}, r.err
+	}
+	return r.Row.Columns()
+}
+
+// ColumnTypes returns the underlying sql.Rows.ColumnTypes(), or the deferred error
+func (r *Row) ColumnTypes() ([]*sql.ColumnType, error) {
+	if r.err != nil {
+		return []*sql.ColumnType{}, r.err
+	}
+	return r.Row.ColumnTypes()
+}
+
+// Err returns the error encountered while scanning.
+func (r *Row) Err() error {
+	return r.err
+}