From 71d4c873bd12233b585637404115cfea9462c904 Mon Sep 17 00:00:00 2001 From: Stefan Majewsky Date: Thu, 30 Apr 2026 00:56:09 +0200 Subject: add ctx arguments to most methods --- query.go | 73 ++++++++++++++++++++++++++++++++-------------------------------- 1 file changed, 37 insertions(+), 36 deletions(-) (limited to 'query.go') diff --git a/query.go b/query.go index 73c2233..b9bc328 100644 --- a/query.go +++ b/query.go @@ -4,6 +4,7 @@ package oblast import ( + "context" "database/sql" "fmt" "reflect" @@ -29,7 +30,7 @@ type preparedStatement struct { } // prepare behaves like [Handle.Prepare]. -func prepare(db Handle, query, operation string, inputSize int) (preparedStatement, error) { +func prepare(ctx context.Context, db Handle, query, operation string, inputSize int) (preparedStatement, error) { if query == "" { return preparedStatement{}, fmt.Errorf("cannot execute %s() because query could not be autogenerated", operation) } @@ -37,7 +38,7 @@ func prepare(db Handle, query, operation string, inputSize int) (preparedStateme if inputSize < PrepareThreshold { return preparedStatement{db, query, nil}, nil } - stmt, err := db.Prepare(query) + stmt, err := db.PrepareContext(ctx, query) if err != nil { return preparedStatement{}, fmt.Errorf("during Prepare(): %w", err) } @@ -52,20 +53,20 @@ func (s preparedStatement) Close() error { return s.stmt.Close() } -// Exec behaves like [sql.Stmt.Exec]. -func (s preparedStatement) Exec(args ...any) (sql.Result, error) { +// ExecContext behaves like [sql.Stmt.ExecContext]. +func (s preparedStatement) ExecContext(ctx context.Context, args ...any) (sql.Result, error) { if s.stmt == nil { - return s.db.Exec(s.query, args...) + return s.db.ExecContext(ctx, s.query, args...) } - return s.stmt.Exec(args...) + return s.stmt.ExecContext(ctx, args...) } -// QueryRow behaves like [sql.Stmt.QueryRow]. -func (s preparedStatement) QueryRow(args ...any) *sql.Row { +// QueryRow behaves like [sql.Stmt.QueryRowContext]. +func (s preparedStatement) QueryRowContext(ctx context.Context, args ...any) *sql.Row { if s.stmt == nil { - return s.db.QueryRow(s.query, args...) + return s.db.QueryRowContext(ctx, s.query, args...) } - return s.stmt.QueryRow(args...) + return s.stmt.QueryRowContext(ctx, args...) } // Insert executes an SQL INSERT statement for each of the provided records. @@ -77,18 +78,18 @@ func (s preparedStatement) QueryRow(args ...any) *sql.Row { // // Returns an error if any of the `records` has a non-zero value in any column marked as `db:",auto"`. // Records that already exist in the database should be handled with [Store.Update] instead. -func (s Store[R]) Insert(db Handle, records ...*R) error { +func (s Store[R]) Insert(ctx context.Context, db Handle, records ...*R) error { // NOTE: This function body should be as short as possible to reduce the binary size after monomorphization. // Any expression that does not depend on type R should be factored out into a reusable function. - stmt, err := prepare(db, s.plan.Insert.Query, "Insert", len(records)) + stmt, err := prepare(ctx, db, s.plan.Insert.Query, "Insert", len(records)) if err != nil { return err } - return s.insertUsing(stmt, db, records) + return s.insertUsing(ctx, stmt, db, records) } -func (s Store[R]) insertUsing(stmt preparedStatement, db Handle, records []*R) error { +func (s Store[R]) insertUsing(ctx context.Context, stmt preparedStatement, db Handle, records []*R) error { // NOTE: This function body should be as short as possible to reduce the binary size after monomorphization. // Any expression that does not depend on type R should be factored out into a reusable function. @@ -101,7 +102,7 @@ func (s Store[R]) insertUsing(stmt preparedStatement, db Handle, records []*R) e for idx, r := range records { v := reflect.ValueOf(r).Elem() - err := insertRecord(v, idx, stmt, argumentIndexes, argumentSlots, scanIndexes, scanSlots) + err := insertRecord(ctx, v, idx, stmt, argumentIndexes, argumentSlots, scanIndexes, scanSlots) if err != nil { return newIOError(err, "Stmt.Close", stmt.Close()) } @@ -110,7 +111,7 @@ func (s Store[R]) insertUsing(stmt preparedStatement, db Handle, records []*R) e return newIOError(nil, "Stmt.Close", stmt.Close()) } -func insertRecord(v reflect.Value, recordIndex int, stmt preparedStatement, argumentIndexes [][]int, argumentSlots []any, scanIndexes [][]int, scanSlots []any) error { +func insertRecord(ctx context.Context, v reflect.Value, recordIndex int, stmt preparedStatement, argumentIndexes [][]int, argumentSlots []any, scanIndexes [][]int, scanSlots []any) error { for idx, index := range argumentIndexes { argumentSlots[idx] = v.FieldByIndex(index).Interface() } @@ -123,10 +124,10 @@ func insertRecord(v reflect.Value, recordIndex int, stmt preparedStatement, argu } var err error if len(scanSlots) == 0 { - _, err = stmt.Exec(argumentSlots...) + _, err = stmt.ExecContext(ctx, argumentSlots...) } else { - // TODO: using QueryRow for inserting is somehow extremely expensive in terms of allocs; other libraries are doing better by limiting themselves to Exec() + LastInsertId() - err = stmt.QueryRow(argumentSlots...).Scan(scanSlots...) + // TODO: using QueryRow for inserting is extremely expensive because database/sql allocates a Rows instance under the hood; other libraries are doing better by limiting themselves to ExecContext() + LastInsertId() + err = stmt.QueryRowContext(ctx, argumentSlots...).Scan(scanSlots...) } if err != nil { return fmt.Errorf("while inserting record with idx = %d: %w", recordIndex, err) @@ -138,11 +139,11 @@ func insertRecord(v reflect.Value, recordIndex int, stmt preparedStatement, argu // Returns [MissingRecordError] if any of the records does not exist in the database, that is, if for any of the records, the database contains no row with the same primary key values. // // Returns an error if [NewStore] was called without the [TableNameIs] or [PrimaryKeyIs] options, which are both required to generate a query for this method. -func (s Store[R]) Update(db Handle, records ...R) error { +func (s Store[R]) Update(ctx context.Context, db Handle, records ...R) error { // NOTE: This function body should be as short as possible to reduce the binary size after monomorphization. // Any expression that does not depend on type R should be factored out into a reusable function. - stmt, err := prepare(db, s.plan.Update.Query, "Update", len(records)) + stmt, err := prepare(ctx, db, s.plan.Update.Query, "Update", len(records)) if err != nil { return err } @@ -154,7 +155,7 @@ func (s Store[R]) Update(db Handle, records ...R) error { for idx := range records { v := reflect.ValueOf(&records[idx]).Elem() - rowsAffected, err := updateRecord(v, idx, stmt, argumentIndexes, argumentSlots) + rowsAffected, err := updateRecord(ctx, v, idx, stmt, argumentIndexes, argumentSlots) if err == nil && rowsAffected == 0 { err = MissingRecordError[R]{records[idx], s.plan} } @@ -165,11 +166,11 @@ func (s Store[R]) Update(db Handle, records ...R) error { return newIOError(nil, "Stmt.Close", stmt.Close()) } -func updateRecord(v reflect.Value, recordIndex int, stmt preparedStatement, argumentIndexes [][]int, argumentSlots []any) (int64, error) { +func updateRecord(ctx context.Context, v reflect.Value, recordIndex int, stmt preparedStatement, argumentIndexes [][]int, argumentSlots []any) (int64, error) { for idx, index := range argumentIndexes { argumentSlots[idx] = v.FieldByIndex(index).Interface() } - result, err := stmt.Exec(argumentSlots...) + result, err := stmt.ExecContext(ctx, argumentSlots...) if err != nil { return 0, fmt.Errorf("while updating record with idx = %d: %w", recordIndex, err) } @@ -183,11 +184,11 @@ func updateRecord(v reflect.Value, recordIndex int, stmt preparedStatement, argu // Delete executes an SQL DELETE statement for each of the provided records, using their primary keys to locate the respective table rows. // // Returns an error if [NewStore] was called without the [TableNameIs] or [PrimaryKeyIs] options, which are both required to generate a query for this method. -func (s Store[R]) Delete(db Handle, records ...R) error { +func (s Store[R]) Delete(ctx context.Context, db Handle, records ...R) error { // NOTE: This function body should be as short as possible to reduce the binary size after monomorphization. // Any expression that does not depend on type R should be factored out into a reusable function. - stmt, err := prepare(db, s.plan.Delete.Query, "Delete", len(records)) + stmt, err := prepare(ctx, db, s.plan.Delete.Query, "Delete", len(records)) if err != nil { return err } @@ -199,7 +200,7 @@ func (s Store[R]) Delete(db Handle, records ...R) error { for idx := range records { v := reflect.ValueOf(&records[idx]).Elem() - err := deleteRecord(v, idx, stmt, argumentIndexes, argumentSlots) + err := deleteRecord(ctx, v, idx, stmt, argumentIndexes, argumentSlots) if err != nil { return newIOError(err, "Stmt.Close", stmt.Close()) } @@ -208,11 +209,11 @@ func (s Store[R]) Delete(db Handle, records ...R) error { return newIOError(nil, "Stmt.Close", stmt.Close()) } -func deleteRecord(v reflect.Value, recordIndex int, stmt preparedStatement, argumentIndexes [][]int, argumentSlots []any) error { +func deleteRecord(ctx context.Context, v reflect.Value, recordIndex int, stmt preparedStatement, argumentIndexes [][]int, argumentSlots []any) error { for idx, index := range argumentIndexes { argumentSlots[idx] = v.FieldByIndex(index).Interface() } - _, err := stmt.Exec(argumentSlots...) + _, err := stmt.ExecContext(ctx, argumentSlots...) if err != nil { return fmt.Errorf("while deleting record with idx = %d: %w", recordIndex, err) } @@ -227,24 +228,24 @@ func deleteRecord(v reflect.Value, recordIndex int, stmt preparedStatement, argu // Returns an error if [NewStore] was called without the [TableNameIs] or [PrimaryKeyIs] options, which are both required to generate the respective queries for this method. // - For record types that do not have fields declared with the "auto" tag, an INSERT ... ON CONFLICT statement is used. // Returns an error if [NewStore] was called without the [TableNameIs] option, which is required to generate a query for this method. -func (s Store[R]) Upsert(db Handle, records ...*R) error { +func (s Store[R]) Upsert(ctx context.Context, db Handle, records ...*R) error { // NOTE: This function body should be as short as possible to reduce the binary size after monomorphization. // Any expression that does not depend on type R should be factored out into a reusable function. if len(s.plan.AutoColumnNames) == 0 { - stmt, err := prepare(db, s.plan.Upsert.Query, "Upsert", len(records)) + stmt, err := prepare(ctx, db, s.plan.Upsert.Query, "Upsert", len(records)) if err != nil { return err } - return s.insertUsing(stmt, db, records) + return s.insertUsing(ctx, stmt, db, records) } // TODO: respect PrepareThreshold (or not? may be too much bookkeeping overhead for not a whole lot of benefit) - insertStmt, err := prepare(db, s.plan.Insert.Query, "Insert", 0) + insertStmt, err := prepare(ctx, db, s.plan.Insert.Query, "Insert", 0) if err != nil { return err } - updateStmt, err := prepare(db, s.plan.Update.Query, "Update", 0) + updateStmt, err := prepare(ctx, db, s.plan.Update.Query, "Update", 0) if err != nil { return err } @@ -266,10 +267,10 @@ func (s Store[R]) Upsert(db Handle, records ...*R) error { } if isInsert { - err = insertRecord(v, idx, insertStmt, insertArgumentIndexes, insertArgumentSlots, insertScanIndexes, insertScanSlots) + err = insertRecord(ctx, v, idx, insertStmt, insertArgumentIndexes, insertArgumentSlots, insertScanIndexes, insertScanSlots) } else { var rowsAffected int64 - rowsAffected, err = updateRecord(v, idx, updateStmt, updateArgumentIndexes, updateArgumentSlots) + rowsAffected, err = updateRecord(ctx, v, idx, updateStmt, updateArgumentIndexes, updateArgumentSlots) if err == nil && rowsAffected == 0 { err = MissingRecordError[R]{*r, s.plan} } -- cgit v1.2.3