feat(query): add Query, MustQuery, and UpdateOne helpers
Adds scan-into-dest query helpers and update-one-row semantics with Context variants, tests, and documentation updates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
### Added
|
||||
|
||||
- `StringKSUID.Equal` / `IsZero`, `BinaryKSUID.Equal` / `IsZero`, and nil sentinel vars for [ApplyInterfacePtr].
|
||||
- `Query`, `MustQuery`, and `UpdateOne` helpers (+ Context variants).
|
||||
- Context-aware variants for all query and mutation helpers.
|
||||
- `Delete`, `DeleteAffected`, `WithTx`, `ContainsCol`, and `CurrentDialect()`.
|
||||
- Package documentation (`doc.go`), expanded README, and subpackage docs for `dbxm` / `dbxp`.
|
||||
|
||||
@@ -42,8 +42,8 @@ For Postgres, use `dbx.DialectPostgres` and blank-import `dbxp` instead of `dbxm
|
||||
|
||||
| Area | Functions |
|
||||
|------|-----------|
|
||||
| Query | `Fetch`, `MustFetch`, `FetchOne`, `MustFetchOne` (+ `*Context` variants) |
|
||||
| Mutations | `Insert`, `InsertReturning`, `Update`, `UpdateAffected`, `UpdateReturning`, `Delete`, `DeleteAffected` (+ `*Context` variants) |
|
||||
| Query | `Fetch`, `MustFetch`, `FetchOne`, `MustFetchOne`, `Query`, `MustQuery` (+ `*Context` variants) |
|
||||
| Mutations | `Insert`, `InsertReturning`, `Update`, `UpdateAffected`, `UpdateOne`, `UpdateReturning`, `Delete`, `DeleteAffected` (+ `*Context` variants) |
|
||||
| Transactions | `WithTx` |
|
||||
| Columns | `NormalCols`, `ContainsCol`, `ExprValues`, `ExprStringers` |
|
||||
| Partial update | `ApplyPtr`, `ApplyComplexPtr`, `ApplyInterfacePtr`, `ApplyVal` |
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
//
|
||||
// # Query and mutation helpers
|
||||
//
|
||||
// [Fetch], [FetchOne], [Insert], [Update], [Delete], and their Must* and Context
|
||||
// variants wrap Jet [Statement] execution with consistent error semantics.
|
||||
// [Fetch], [FetchOne], [Query], [Insert], [Update], [Delete], and their Must*
|
||||
// and Context variants wrap Jet [Statement] execution with consistent error
|
||||
// semantics. [UpdateOne] requires at least one row affected.
|
||||
// [WithTx] runs a function inside a SQL transaction.
|
||||
//
|
||||
// # Jet column utilities
|
||||
|
||||
@@ -74,6 +74,24 @@ func UpdateAffectedContext(ctx context.Context, sqlo Executable, stmt Statement)
|
||||
return rowsAffected, nil
|
||||
}
|
||||
|
||||
// UpdateOne executes an update statement and returns notFoundErr if zero rows
|
||||
// were affected.
|
||||
func UpdateOne(sqlo Executable, stmt Statement, notFoundErr error) error {
|
||||
return UpdateOneContext(context.Background(), sqlo, stmt, notFoundErr)
|
||||
}
|
||||
|
||||
// UpdateOneContext is the context-aware variant of [UpdateOne].
|
||||
func UpdateOneContext(ctx context.Context, sqlo Executable, stmt Statement, notFoundErr error) error {
|
||||
n, err := UpdateAffectedContext(ctx, sqlo, stmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return notFoundErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateReturning executes an update statement that returns the updated row.
|
||||
// The statement MUST be a Jet UpdateStatement with a RETURNING clause. Returns
|
||||
// the updated row object T or an error if the update fails or no rows are returned.
|
||||
|
||||
@@ -60,6 +60,29 @@ func TestUpdateAffected_ReturnsCount(t *testing.T) {
|
||||
assert.Equal(t, int64(3), n)
|
||||
}
|
||||
|
||||
func TestUpdateOne_NotFound(t *testing.T) {
|
||||
errNotFound := errors.New("not found")
|
||||
stmt := mockStatement{
|
||||
execContextFn: func(_ context.Context) (sql.Result, error) {
|
||||
return mockResult{rowsAffected: 0}, nil
|
||||
},
|
||||
}
|
||||
|
||||
err := UpdateOne(mockExecutable{}, stmt, errNotFound)
|
||||
assert.ErrorIs(t, err, errNotFound)
|
||||
}
|
||||
|
||||
func TestUpdateOne_Success(t *testing.T) {
|
||||
stmt := mockStatement{
|
||||
execContextFn: func(_ context.Context) (sql.Result, error) {
|
||||
return mockResult{rowsAffected: 1}, nil
|
||||
},
|
||||
}
|
||||
|
||||
err := UpdateOne(mockExecutable{}, stmt, errors.New("not found"))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDelete_Succeeds(t *testing.T) {
|
||||
called := false
|
||||
stmt := mockStatement{
|
||||
|
||||
@@ -70,3 +70,31 @@ func MustFetchOneContext[T any](ctx context.Context, sqlo Queryable, stmt Statem
|
||||
}
|
||||
return result[0], nil
|
||||
}
|
||||
|
||||
// Query executes a Jet statement and scans the result into dest.
|
||||
func Query(sqlo Queryable, stmt Statement, dest any) error {
|
||||
return QueryContext(context.Background(), sqlo, stmt, dest)
|
||||
}
|
||||
|
||||
// QueryContext is the context-aware variant of [Query].
|
||||
func QueryContext(ctx context.Context, sqlo Queryable, stmt Statement, dest any) error {
|
||||
return stmt.QueryContext(ctx, sqlo, dest)
|
||||
}
|
||||
|
||||
// MustQuery executes a Jet statement and scans the result into dest. If the
|
||||
// query returns no rows, notFoundErr is returned instead of [ErrNoRows].
|
||||
func MustQuery(sqlo Queryable, stmt Statement, dest any, notFoundErr error) error {
|
||||
return MustQueryContext(context.Background(), sqlo, stmt, dest, notFoundErr)
|
||||
}
|
||||
|
||||
// MustQueryContext is the context-aware variant of [MustQuery].
|
||||
func MustQueryContext(ctx context.Context, sqlo Queryable, stmt Statement, dest any, notFoundErr error) error {
|
||||
err := stmt.QueryContext(ctx, sqlo, dest)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNoRows) {
|
||||
return notFoundErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -91,3 +91,45 @@ func TestMustFetchOne_NotFound(t *testing.T) {
|
||||
assert.Nil(t, result)
|
||||
assert.ErrorIs(t, err, errNotFound)
|
||||
}
|
||||
|
||||
func TestQuery_ScansDest(t *testing.T) {
|
||||
stmt := mockStatement{
|
||||
queryContextFn: func(_ context.Context, dest any) error {
|
||||
ptr := dest.(*row)
|
||||
ptr.ID = 5
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var result row
|
||||
err := Query(mockQueryable{}, stmt, &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 5, result.ID)
|
||||
}
|
||||
|
||||
func TestMustQuery_NotFound(t *testing.T) {
|
||||
errNotFound := errors.New("not found")
|
||||
stmt := mockStatement{
|
||||
queryContextFn: func(_ context.Context, _ any) error {
|
||||
return ErrNoRows
|
||||
},
|
||||
}
|
||||
|
||||
var result row
|
||||
err := MustQuery(mockQueryable{}, stmt, &result, errNotFound)
|
||||
assert.ErrorIs(t, err, errNotFound)
|
||||
}
|
||||
|
||||
func TestMustQueryContext_PropagatesContext(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), testContextKey{}, "ok")
|
||||
var gotCtx context.Context
|
||||
stmt := mockStatement{
|
||||
queryContextFn: func(c context.Context, dest any) error {
|
||||
gotCtx = c
|
||||
return ErrNoRows
|
||||
},
|
||||
}
|
||||
|
||||
_ = MustQueryContext(ctx, mockQueryable{}, stmt, &row{}, errors.New("nf"))
|
||||
assert.Equal(t, ctx, gotCtx)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user