mirror of
https://github.com/hanzoai/orm.git
synced 2026-10-30 15:01:22 +00:00
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.
37 lines
1.2 KiB
Go
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")
|
|
}
|
|
}
|