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 }