github.com/letsencrypt/boulder@v0.20251208.0/db/map.go (about)

     1  package db
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"errors"
     7  	"fmt"
     8  	"reflect"
     9  	"regexp"
    10  
    11  	"github.com/go-sql-driver/mysql"
    12  	"github.com/letsencrypt/borp"
    13  )
    14  
    15  // ErrDatabaseOp wraps an underlying err with a description of the operation
    16  // that was being performed when the error occurred (insert, select, select
    17  // one, exec, etc) and the table that the operation was being performed on.
    18  type ErrDatabaseOp struct {
    19  	Op    string
    20  	Table string
    21  	Err   error
    22  }
    23  
    24  // Error for an ErrDatabaseOp composes a message with context about the
    25  // operation and table as well as the underlying Err's error message.
    26  func (e ErrDatabaseOp) Error() string {
    27  	// If there is a table, include it in the context
    28  	if e.Table != "" {
    29  		return fmt.Sprintf(
    30  			"failed to %s %s: %s",
    31  			e.Op,
    32  			e.Table,
    33  			e.Err)
    34  	}
    35  	return fmt.Sprintf(
    36  		"failed to %s: %s",
    37  		e.Op,
    38  		e.Err)
    39  }
    40  
    41  // Unwrap returns the inner error to allow inspection of error chains.
    42  func (e ErrDatabaseOp) Unwrap() error {
    43  	return e.Err
    44  }
    45  
    46  // IsNoRows is a utility function for determining if an error wraps the go sql
    47  // package's ErrNoRows, which is returned when a Scan operation has no more
    48  // results to return, and as such is returned by many borp methods.
    49  func IsNoRows(err error) bool {
    50  	return errors.Is(err, sql.ErrNoRows)
    51  }
    52  
    53  // IsDuplicate is a utility function for determining if an error wrap MySQL's
    54  // Error 1062: Duplicate entry. This error is returned when inserting a row
    55  // would violate a unique key constraint.
    56  func IsDuplicate(err error) bool {
    57  	var dbErr *mysql.MySQLError
    58  	return errors.As(err, &dbErr) && dbErr.Number == 1062
    59  }
    60  
    61  // WrappedMap wraps a *borp.DbMap such that its major functions wrap error
    62  // results in ErrDatabaseOp instances before returning them to the caller.
    63  type WrappedMap struct {
    64  	dbMap *borp.DbMap
    65  }
    66  
    67  func NewWrappedMap(dbMap *borp.DbMap) *WrappedMap {
    68  	return &WrappedMap{dbMap: dbMap}
    69  }
    70  
    71  func (m *WrappedMap) TableFor(t reflect.Type, checkPK bool) (*borp.TableMap, error) {
    72  	return m.dbMap.TableFor(t, checkPK)
    73  }
    74  
    75  func (m *WrappedMap) Get(ctx context.Context, holder any, keys ...any) (any, error) {
    76  	return WrappedExecutor{sqlExecutor: m.dbMap}.Get(ctx, holder, keys...)
    77  }
    78  
    79  func (m *WrappedMap) Insert(ctx context.Context, list ...any) error {
    80  	return WrappedExecutor{sqlExecutor: m.dbMap}.Insert(ctx, list...)
    81  }
    82  
    83  func (m *WrappedMap) Update(ctx context.Context, list ...any) (int64, error) {
    84  	return WrappedExecutor{sqlExecutor: m.dbMap}.Update(ctx, list...)
    85  }
    86  
    87  func (m *WrappedMap) Delete(ctx context.Context, list ...any) (int64, error) {
    88  	return WrappedExecutor{sqlExecutor: m.dbMap}.Delete(ctx, list...)
    89  }
    90  
    91  func (m *WrappedMap) Select(ctx context.Context, holder any, query string, args ...any) ([]any, error) {
    92  	return WrappedExecutor{sqlExecutor: m.dbMap}.Select(ctx, holder, query, args...)
    93  }
    94  
    95  func (m *WrappedMap) SelectOne(ctx context.Context, holder any, query string, args ...any) error {
    96  	return WrappedExecutor{sqlExecutor: m.dbMap}.SelectOne(ctx, holder, query, args...)
    97  }
    98  
    99  func (m *WrappedMap) SelectNullInt(ctx context.Context, query string, args ...any) (sql.NullInt64, error) {
   100  	return WrappedExecutor{sqlExecutor: m.dbMap}.SelectNullInt(ctx, query, args...)
   101  }
   102  
   103  func (m *WrappedMap) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
   104  	return WrappedExecutor{sqlExecutor: m.dbMap}.QueryContext(ctx, query, args...)
   105  }
   106  
   107  func (m *WrappedMap) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
   108  	return WrappedExecutor{sqlExecutor: m.dbMap}.QueryRowContext(ctx, query, args...)
   109  }
   110  
   111  func (m *WrappedMap) SelectStr(ctx context.Context, query string, args ...any) (string, error) {
   112  	return WrappedExecutor{sqlExecutor: m.dbMap}.SelectStr(ctx, query, args...)
   113  }
   114  
   115  func (m *WrappedMap) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
   116  	return WrappedExecutor{sqlExecutor: m.dbMap}.ExecContext(ctx, query, args...)
   117  }
   118  
   119  func (m *WrappedMap) BeginTx(ctx context.Context) (Transaction, error) {
   120  	tx, err := m.dbMap.BeginTx(ctx)
   121  	if err != nil {
   122  		return tx, ErrDatabaseOp{
   123  			Op:  "begin transaction",
   124  			Err: err,
   125  		}
   126  	}
   127  	return WrappedTransaction{
   128  		transaction: tx,
   129  	}, err
   130  }
   131  
   132  func (m *WrappedMap) ColumnsForModel(model any) ([]string, error) {
   133  	tbl, err := m.dbMap.TableFor(reflect.TypeOf(model), true)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  	var columns []string
   138  	for _, col := range tbl.Columns {
   139  		columns = append(columns, col.ColumnName)
   140  	}
   141  	return columns, nil
   142  }
   143  
   144  // WrappedTransaction wraps a *borp.Transaction such that its major functions
   145  // wrap error results in ErrDatabaseOp instances before returning them to the
   146  // caller.
   147  type WrappedTransaction struct {
   148  	transaction *borp.Transaction
   149  }
   150  
   151  func (tx WrappedTransaction) Commit() error {
   152  	return tx.transaction.Commit()
   153  }
   154  
   155  func (tx WrappedTransaction) Rollback() error {
   156  	return tx.transaction.Rollback()
   157  }
   158  
   159  func (tx WrappedTransaction) Get(ctx context.Context, holder any, keys ...any) (any, error) {
   160  	return (WrappedExecutor{sqlExecutor: tx.transaction}).Get(ctx, holder, keys...)
   161  }
   162  
   163  func (tx WrappedTransaction) Insert(ctx context.Context, list ...any) error {
   164  	return (WrappedExecutor{sqlExecutor: tx.transaction}).Insert(ctx, list...)
   165  }
   166  
   167  func (tx WrappedTransaction) Update(ctx context.Context, list ...any) (int64, error) {
   168  	return (WrappedExecutor{sqlExecutor: tx.transaction}).Update(ctx, list...)
   169  }
   170  
   171  func (tx WrappedTransaction) Delete(ctx context.Context, list ...any) (int64, error) {
   172  	return (WrappedExecutor{sqlExecutor: tx.transaction}).Delete(ctx, list...)
   173  }
   174  
   175  func (tx WrappedTransaction) Select(ctx context.Context, holder any, query string, args ...any) ([]any, error) {
   176  	return (WrappedExecutor{sqlExecutor: tx.transaction}).Select(ctx, holder, query, args...)
   177  }
   178  
   179  func (tx WrappedTransaction) SelectOne(ctx context.Context, holder any, query string, args ...any) error {
   180  	return (WrappedExecutor{sqlExecutor: tx.transaction}).SelectOne(ctx, holder, query, args...)
   181  }
   182  
   183  func (tx WrappedTransaction) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
   184  	return (WrappedExecutor{sqlExecutor: tx.transaction}).QueryContext(ctx, query, args...)
   185  }
   186  
   187  func (tx WrappedTransaction) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
   188  	return (WrappedExecutor{sqlExecutor: tx.transaction}).ExecContext(ctx, query, args...)
   189  }
   190  
   191  // WrappedExecutor wraps a borp.SqlExecutor such that its major functions
   192  // wrap error results in ErrDatabaseOp instances before returning them to the
   193  // caller.
   194  type WrappedExecutor struct {
   195  	sqlExecutor borp.SqlExecutor
   196  }
   197  
   198  func errForOp(operation string, err error, list []any) ErrDatabaseOp {
   199  	table := "unknown"
   200  	if len(list) > 0 {
   201  		table = fmt.Sprintf("%T", list[0])
   202  	}
   203  	return ErrDatabaseOp{
   204  		Op:    operation,
   205  		Table: table,
   206  		Err:   err,
   207  	}
   208  }
   209  
   210  func errForQuery(query, operation string, err error, list []any) ErrDatabaseOp {
   211  	// Extract the table from the query
   212  	table := tableFromQuery(query)
   213  	if table == "" && len(list) > 0 {
   214  		// If there's no table from the query but there was a list of holder types,
   215  		// use the type from the first element of the list and indicate we failed to
   216  		// extract a table from the query.
   217  		table = fmt.Sprintf("%T (unknown table)", list[0])
   218  	} else if table == "" {
   219  		// If there's no table from the query and no list of holders then all we can
   220  		// say is that the table is unknown.
   221  		table = "unknown table"
   222  	}
   223  
   224  	return ErrDatabaseOp{
   225  		Op:    operation,
   226  		Table: table,
   227  		Err:   err,
   228  	}
   229  }
   230  
   231  func (we WrappedExecutor) Get(ctx context.Context, holder any, keys ...any) (any, error) {
   232  	res, err := we.sqlExecutor.Get(ctx, holder, keys...)
   233  	if err != nil {
   234  		return res, errForOp("get", err, []any{holder})
   235  	}
   236  	return res, err
   237  }
   238  
   239  func (we WrappedExecutor) Insert(ctx context.Context, list ...any) error {
   240  	err := we.sqlExecutor.Insert(ctx, list...)
   241  	if err != nil {
   242  		return errForOp("insert", err, list)
   243  	}
   244  	return nil
   245  }
   246  
   247  func (we WrappedExecutor) Update(ctx context.Context, list ...any) (int64, error) {
   248  	updatedRows, err := we.sqlExecutor.Update(ctx, list...)
   249  	if err != nil {
   250  		return updatedRows, errForOp("update", err, list)
   251  	}
   252  	return updatedRows, err
   253  }
   254  
   255  func (we WrappedExecutor) Delete(ctx context.Context, list ...any) (int64, error) {
   256  	deletedRows, err := we.sqlExecutor.Delete(ctx, list...)
   257  	if err != nil {
   258  		return deletedRows, errForOp("delete", err, list)
   259  	}
   260  	return deletedRows, err
   261  }
   262  
   263  func (we WrappedExecutor) Select(ctx context.Context, holder any, query string, args ...any) ([]any, error) {
   264  	result, err := we.sqlExecutor.Select(ctx, holder, query, args...)
   265  	if err != nil {
   266  		return result, errForQuery(query, "select", err, []any{holder})
   267  	}
   268  	return result, err
   269  }
   270  
   271  func (we WrappedExecutor) SelectOne(ctx context.Context, holder any, query string, args ...any) error {
   272  	err := we.sqlExecutor.SelectOne(ctx, holder, query, args...)
   273  	if err != nil {
   274  		return errForQuery(query, "select one", err, []any{holder})
   275  	}
   276  	return nil
   277  }
   278  
   279  func (we WrappedExecutor) SelectNullInt(ctx context.Context, query string, args ...any) (sql.NullInt64, error) {
   280  	rows, err := we.sqlExecutor.SelectNullInt(ctx, query, args...)
   281  	if err != nil {
   282  		return sql.NullInt64{}, errForQuery(query, "select", err, nil)
   283  	}
   284  	return rows, nil
   285  }
   286  
   287  func (we WrappedExecutor) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
   288  	// Note: we can't do error wrapping here because the error is passed via the `*sql.Row`
   289  	// object, and we can't produce a `*sql.Row` object with a custom error because it is unexported.
   290  	return we.sqlExecutor.QueryRowContext(ctx, query, args...)
   291  }
   292  
   293  func (we WrappedExecutor) SelectStr(ctx context.Context, query string, args ...any) (string, error) {
   294  	str, err := we.sqlExecutor.SelectStr(ctx, query, args...)
   295  	if err != nil {
   296  		return "", errForQuery(query, "select", err, nil)
   297  	}
   298  	return str, nil
   299  }
   300  
   301  func (we WrappedExecutor) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
   302  	rows, err := we.sqlExecutor.QueryContext(ctx, query, args...)
   303  	if err != nil {
   304  		return nil, errForQuery(query, "select", err, nil)
   305  	}
   306  	return rows, nil
   307  }
   308  
   309  var (
   310  	// selectTableRegexp matches the table name from an SQL select statement
   311  	selectTableRegexp = regexp.MustCompile(`(?i)^\s*select\s+[a-z\d:\.\(\), \_\*` + "`" + `]+\s+from\s+([a-z\d\_,` + "`" + `]+)`)
   312  	// insertTableRegexp matches the table name from an SQL insert statement
   313  	insertTableRegexp = regexp.MustCompile(`(?i)^\s*insert\s+into\s+([a-z\d \_,` + "`" + `]+)\s+(?:set|\()`)
   314  	// updateTableRegexp matches the table name from an SQL update statement
   315  	updateTableRegexp = regexp.MustCompile(`(?i)^\s*update\s+([a-z\d \_,` + "`" + `]+)\s+set`)
   316  	// deleteTableRegexp matches the table name from an SQL delete statement
   317  	deleteTableRegexp = regexp.MustCompile(`(?i)^\s*delete\s+from\s+([a-z\d \_,` + "`" + `]+)\s+where`)
   318  
   319  	// tableRegexps is a list of regexps that tableFromQuery will try to use in
   320  	// succession to find the table name for an SQL query. While tableFromQuery
   321  	// isn't used by the higher level borp Insert/Update/Select/etc functions we
   322  	// include regexps for matching inserts, updates, selects, etc because we want
   323  	// to match the correct table when these types of queries are run through
   324  	// ExecContext().
   325  	tableRegexps = []*regexp.Regexp{
   326  		selectTableRegexp,
   327  		insertTableRegexp,
   328  		updateTableRegexp,
   329  		deleteTableRegexp,
   330  	}
   331  )
   332  
   333  // tableFromQuery uses the tableRegexps on the provided query to return the
   334  // associated table name or an empty string if it can't be determined from the
   335  // query.
   336  func tableFromQuery(query string) string {
   337  	for _, r := range tableRegexps {
   338  		if matches := r.FindStringSubmatch(query); len(matches) >= 2 {
   339  			return matches[1]
   340  		}
   341  	}
   342  	return ""
   343  }
   344  
   345  func (we WrappedExecutor) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
   346  	res, err := we.sqlExecutor.ExecContext(ctx, query, args...)
   347  	if err != nil {
   348  		return res, errForQuery(query, "exec", err, args)
   349  	}
   350  	return res, nil
   351  }