Files
zeekay 8087c40f65 the four backends are one contract, and a suite now holds them to it
SQLite, hanzo/sql, hanzo/docdb and hanzo/datastore satisfy one DB interface, and
nothing checked that they agree. The only cross-backend test was gated on an
address variable and skipped, so a driver could diverge for a release with every
run green.

The behaviours are written once and run against every backend that answers. One
that nobody can reach is REPORTED by name, because a green run that quietly
exercised a single backend is not evidence about the others. SQLite needs no
server and is held separately, so an under-provisioned run cannot pass by
skipping everything.

It found a portability bug on its first run: IsNotFound did not recognise
ErrNoSuchEntity, which is what every backend opened through OpenSQLite and
OpenZap actually returns. The predicate that exists so callers need not know
which layer noticed answered false for a plain absent row, so code written the
portable way read "not there" as "failed". Its own comment named the ZAP, KV and
document backends while omitting the sentinel they use. Invisible until now
because each backend was only ever tested against itself.
2026-08-21 08:47:03 -07:00

60 lines
2.5 KiB
Go

package orm
import (
"database/sql"
"errors"
ormdb "github.com/hanzoai/orm/db"
)
var (
// ErrNotFound is returned when an entity is not found.
ErrNotFound = errors.New("orm: entity not found")
// ErrKindMismatch is re-exported from the db layer: CreateIfAbsent returns it
// when the id is already held by a row of a different kind. One sentinel, so
// errors.Is matches whether callers compare against orm.ErrKindMismatch or
// ormdb.ErrKindMismatch.
ErrKindMismatch = ormdb.ErrKindMismatch
// ErrAlreadyRegistered is returned when a kind is registered twice.
ErrAlreadyRegistered = errors.New("orm: kind already registered")
// ErrTxPanic is the sentinel returned from RunInTransactionWith when
// the callback panicked. The ORM rolls back the transaction, logs
// the panic with the attempt index, and re-panics to the caller so
// the defect surfaces in logs with the full goroutine trace.
// Consumers should never see this error directly — it is followed
// immediately by a re-panic.
ErrTxPanic = errors.New("orm: tx callback panicked")
)
// IsNotFound reports whether err means "no such entity", whichever backend said
// so.
//
// THREE sentinels are the same fact told by different layers: orm's own
// ErrNotFound, the db layer's ErrNoSuchEntity — which is what every backend
// opened through OpenSQLite/OpenZap actually returns — and database/sql's
// ErrNoRows from any store built on a SQL driver. A caller wants to know that
// the thing is not there; which layer noticed is not its business.
//
// ErrNoSuchEntity was missing here, which made this predicate answer false for a
// not-found on the backend everything uses by default. Code written the portable
// way read "absent" as "failed", and the gap was invisible because each backend
// was only ever tested against itself.
//
// It is a PREDICATE and not a wrapping, deliberately. Making ErrNotFound wrap
// sql.ErrNoRows would weld orm's public sentinel to database/sql, which is a lie
// on the ZAP, KV and document backends where no SQL exists and no rows were
// consulted. The value is "not found", not "SQL returned no rows".
//
// It also lets a store migrate one layer at a time: callers can ask this today
// while the store beneath them still returns sql.ErrNoRows, and keep asking it
// unchanged after the store moves to orm. Without it, changing what a store
// returns is a flag day across every caller at once.
func IsNotFound(err error) bool {
return errors.Is(err, ErrNotFound) ||
errors.Is(err, ormdb.ErrNoSuchEntity) ||
errors.Is(err, sql.ErrNoRows)
}