Files
orm/notfound_test.go
zeekay 741f67f5d3 not-found is one question, and raw SQL gets a name you can count
IsNotFound answers "is the thing there" across both layers. A store built on a SQL
driver returns sql.ErrNoRows and orm returns ErrNotFound; callers want the fact,
not which layer noticed it. Until now a store could not change what it returned
without every caller changing in the same commit.

It is a predicate rather than a wrapping. Making ErrNotFound wrap sql.ErrNoRows
would weld orm's public sentinel to database/sql, which is false on the ZAP, KV and
document backends, where no SQL exists and no rows were consulted. A test pins the
two as unrelated, so a later "simplification" cannot join them.

query.Raw is NewQuery under a name that can be audited. Both build the same thing;
only one can be found. `grep -r 'query.Raw'` is the inventory of SQL the builder
cannot express, and its doc names the four constructs that earn it — RETURNING,
FTS5, caller-owned vec0, and a dialect gap. Upsert is not among them: dbx has the
builder, and the refusal it used to return on SQLite was a missing override in a
package we own.
2026-08-14 19:19:05 -07:00

37 lines
1.2 KiB
Go

package orm
import (
"database/sql"
"errors"
"fmt"
"testing"
)
// TestIsNotFoundSpansBothLayers is the point of the predicate: a caller asks
// "is it there" without knowing which layer answered. It also pins that the two
// sentinels stay UNRELATED — welding them would make orm's public error claim SQL
// on backends that have none.
func TestIsNotFoundSpansBothLayers(t *testing.T) {
for _, tc := range []struct {
name string
err error
want bool
}{
{"orm's own", ErrNotFound, true},
{"sql's", sql.ErrNoRows, true},
{"wrapped orm", fmt.Errorf("load user: %w", ErrNotFound), true},
{"wrapped sql", fmt.Errorf("scan row: %w", sql.ErrNoRows), true},
{"some other failure", errors.New("connection refused"), false},
{"nil", nil, false},
} {
if got := IsNotFound(tc.err); got != tc.want {
t.Errorf("IsNotFound(%s) = %v, want %v", tc.name, got, tc.want)
}
}
// The sentinels are two values, not one wearing two names. If these ever
// match, someone has wrapped one in the other and orm's error now asserts SQL.
if errors.Is(ErrNotFound, sql.ErrNoRows) || errors.Is(sql.ErrNoRows, ErrNotFound) {
t.Error("ErrNotFound and sql.ErrNoRows must stay unrelated; the predicate is what joins them")
}
}