mirror of
https://github.com/hanzoai/orm.git
synced 2026-10-30 15:01:22 +00:00
* feat(query): re-export dbx SQL AST under orm/query namespace
Every type is an identity alias (type X = dbx.X) and every constructor
is a thin package-level var bound to dbx. Values produced via orm/query
are bit-identical to values produced via dbx and pass through any type
boundary without conversion. Consumers can migrate from hanzoai/dbx to
hanzoai/orm/query file-by-file with zero coordination cost.
* feat: Typed[T] generic wrapper for type-safe SelectQuery binding
Typed[T] is a thin shell around *query.SelectQuery that binds result
rows to a known Go type T. All/One/First/Count terminators replace the
untyped interface{} ceremony in dbx's raw API. orm.Select[T](db, table)
is the common-case constructor; orm.NewTyped[T](sq) wraps any existing
*query.SelectQuery. The escape hatch Typed.Query() exposes the raw
SelectQuery for dbx methods not mirrored on Typed (joins, aggregates,
window functions, per-query hooks).
* test: verify re-export identity and Typed[T] round-trip
query/query_test.go proves:
- type aliases are identity (assignability both directions, reflect
type equality) so the migration is a no-op at the type system level
- Expression builders produce byte-identical SQL and bound params when
invoked via dbx or via query (NewExp, Not, And, Or, In, NotIn, Like,
NotLike, Between, NotBetween, Enclose)
- values cross the dbx / query boundary without conversion
typed_test.go proves:
- All returns []T with correct filtering / ordering
- One returns *T or ErrNotFound on empty
- First returns (T, ok, error) without treating empty as error
- Typed.Query() escape hatch works for unmirrored dbx methods
- NewTyped wraps any externally-built SelectQuery
* docs: SQL query guide + v0.5.0 changelog
docs/sql-query.md explains when to use orm.Typed[T] vs orm/query
directly, gives the two-sed migration recipe from hanzoai/dbx, and
lists what is intentionally not re-exported (the //go:build ignore
compat layer).
CHANGELOG.md flags v0.4.0 existed as a KV/entity-only release; v0.5.0
is the additive SQL slice that unblocks hanzoai/base migrating off
direct dbx imports.
* chore: bump hanzoai/dbx to v1.16.0
Pulls in SelectQuery.Copy() — the deep-copy primitive used by
Typed[T] to keep Count() non-mutating and chain methods race-safe
(Red P6-C1, P6-H1).
* fix(typed): race-safe immutable chain, non-mutating Count, ctx plumbed
Typed[T] is now effectively immutable. Every chain method (Where,
AndWhere, OrWhere, OrderBy, Limit, Offset) returns a Typed[T] wrapping
a CLONE of the underlying *SelectQuery. Sharing a *Typed[T] across
goroutines is safe — no terminator or chain method mutates the
receiver.
Concrete fixes vs Red Part 6:
- P6-C1 (CRITICAL): Count() rewrites SELECT to COUNT(*) on a clone, so
the caller's original projection is intact for subsequent
.All() / .One() calls. The pagination pattern
(tq := ...; tq.Count(); tq.All()) returns real rows, not zero-valued
garbage. Guarded by TestCountDoesNotMutateQuery.
- P6-H1 (HIGH): concurrent .All() + .Count() on a shared *Typed[T] is
race-free. Each terminator operates on a fresh clone; the receiver's
*SelectQuery is never touched by the chain. Guarded by
TestTypedConcurrentSafe under -race.
- P6-M2 (MEDIUM): every terminator threads the passed ctx via
*SelectQuery.WithContext so cancellation and deadlines reach the
driver. No API change.
- P6-M3 (MEDIUM): orm.Select[T](nil, ...) and orm.NewTyped[T](nil) no
longer panic — they return a Typed[T] carrying ErrNilDB that every
terminator surfaces as an error. Guarded by TestSelectNilDBReturnsError
and TestNewTypedNilQuery.
Typed[T].Clone() is added as a public escape hatch for callers that
reach into Typed[T].Query() and want a mutation-isolated copy.
Depends on hanzoai/dbx v1.16.0 for SelectQuery.Copy().
Test: go test -race -count=5 ./... passes clean (zero races, zero
fails).
* feat(orm): SafeHashExp validates keys against injection (P6-H2)
dbx.SqliteBuilder.QuoteSimpleColumnName passes any string containing a
backtick through unchanged, so a query.HashExp key like
id\`, (SELECT 1), \`x
builds to
id\`, (SELECT 1), \`x = {:p0}
— a SQL injection. SafeHashExp is the defense-in-depth boundary for
untrusted column identifiers: keys must match
^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$
anything else is rejected with a clear error identifying the offending
key. Values remain unvalidated because dbx parameterizes them — the
injection surface is exclusively in the keys.
MustSafeHashExp is the panicking flavor for static call sites where
invalid keys are a programming bug.
query.HashExp continues to exist unchanged for developer-controlled
keys. Application code that reads column names from HTTP query strings
or config must go through SafeHashExp.
Test: TestSafeHashExpRejectsInjection covers the 4 Red-proof payloads
plus space/NUL/newline/empty variants. TestSafeHashExpRoundTrip proves
the valid path still builds correct SQL.
* docs: v0.5.1 changelog, SafeHashExp guidance, softened dbx language (P6-H3)
- CHANGELOG.md gains a v0.5.1 section summarizing the P6-C1 / P6-H1 /
P6-H2 / P6-M2 / P6-M3 / P6-H3 fixes and the dbx v1.16.0 bump.
- CHANGELOG.md v0.5.0 "Deprecated" section is reframed as "Migration
posture": direct hanzoai/dbx imports are not deprecated and continue
to work; new code should prefer orm/query. This matches the reality
that base/feat/orm-query-seam still has 68 files importing dbx
directly and a hard deprecation would make those files instantly
non-conforming without a tracked migration PR (Red P6-H3).
- docs/sql-query.md gains a "HashExp keys — developer-controlled only"
section with the backtick-breakout attack payload and the
SafeHashExp remediation pattern. The previous blanket "do not
import hanzoai/dbx directly from application code" is softened to
"new code should import orm/query".
* test: re-register init()-bound types after resetRegistry
TestResetRegistry wipes the model registry to exercise the teardown
path, then re-registers every type it knows about. Three types were
missing: intgUser / intgHooked (adapter_test.go init()) and
UpdateHookedModel (gaps_test.go init()). Without them, running the
full suite under -count>=2 hit a "type not registered" panic in
TestSQLiteAdapterCreate and TestUpdateHookReceivesOldState.
Pre-existing bug exposed while satisfying -race -count=5 on the
P6 regression gate.