mirror of
https://github.com/hanzoai/orm.git
synced 2026-10-30 15:01:22 +00:00
Typed[T].Count swapped the projection for COUNT(*) and kept every other
clause. SQLite accepted the ORDER BY left beside the aggregate; PostgreSQL
refuses it ("must appear in the GROUP BY clause"), so every ordered Count
failed on hanzo-sql alone. A grouped query answered one group's count, and
LIMIT and DISTINCT were ignored on both. Count now runs the query itself as
a subquery of SELECT COUNT(*), with the order dropped.
It was found by running the relational tests on PostgreSQL, which they now
do on every run: internal/pgtest starts a throwaway server from the
binaries on the machine, on a Unix socket in a temporary directory, and a
machine without one says so by name.
replicated/go.mod was untidy against the root module, which failed its
build gate on main.
Co-authored-by: Hanzo Dev <[email protected]>
170 lines
4.2 KiB
Go
170 lines
4.2 KiB
Go
package orm_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"testing"
|
|
|
|
_ "github.com/hanzoai/sqlite"
|
|
|
|
"github.com/hanzoai/orm"
|
|
"github.com/hanzoai/orm/query"
|
|
)
|
|
|
|
// typedUser is the row shape for Typed[T] round-trip tests. Fields are
|
|
// lowercase because dbx's default FieldMapFunc maps CamelCase → snake_case
|
|
// and the test table is seeded with snake_case columns.
|
|
type typedUser struct {
|
|
ID string `db:"id"`
|
|
Email string `db:"email"`
|
|
Active bool `db:"active"`
|
|
}
|
|
|
|
// seedTypedDB returns a fresh in-memory sqlite handle with three users.
|
|
func seedTypedDB(t *testing.T) *query.DB {
|
|
t.Helper()
|
|
|
|
sqlDB, err := sql.Open("sqlite", ":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = sqlDB.Close() })
|
|
|
|
if _, err := sqlDB.Exec(`
|
|
CREATE TABLE users (
|
|
id TEXT PRIMARY KEY,
|
|
email TEXT NOT NULL,
|
|
active INTEGER NOT NULL
|
|
)
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
rows := [][]any{
|
|
{"u1", "[email protected]", 1},
|
|
{"u2", "[email protected]", 1},
|
|
{"u3", "[email protected]", 0},
|
|
}
|
|
for _, r := range rows {
|
|
if _, err := sqlDB.Exec(`INSERT INTO users (id, email, active) VALUES (?, ?, ?)`, r...); err != nil {
|
|
t.Fatalf("seed: %v", err)
|
|
}
|
|
}
|
|
|
|
return query.NewFromDB(sqlDB, "sqlite")
|
|
}
|
|
|
|
func TestTypedAll(t *testing.T) {
|
|
relational(t, func(t *testing.T, db *query.DB) {
|
|
|
|
got, err := orm.Select[typedUser](db, "users").
|
|
Where(query.HashExp{"active": true}).
|
|
OrderBy("email").
|
|
All(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("All: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("want 2 active users, got %d", len(got))
|
|
}
|
|
if got[0].Email != "[email protected]" || got[1].Email != "[email protected]" {
|
|
t.Errorf("unexpected order / values: %+v", got)
|
|
}
|
|
if !got[0].Active || !got[1].Active {
|
|
t.Errorf("inactive users leaked into result")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestTypedOne(t *testing.T) {
|
|
relational(t, func(t *testing.T, db *query.DB) {
|
|
|
|
got, err := orm.Select[typedUser](db, "users").
|
|
Where(query.HashExp{"id": "u2"}).
|
|
One(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("One: %v", err)
|
|
}
|
|
if got == nil || got.Email != "[email protected]" {
|
|
t.Errorf("got %+v", got)
|
|
}
|
|
|
|
_, err = orm.Select[typedUser](db, "users").
|
|
Where(query.HashExp{"id": "nope"}).
|
|
One(context.Background())
|
|
if !errors.Is(err, orm.ErrNotFound) {
|
|
t.Errorf("want ErrNotFound, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestTypedFirst(t *testing.T) {
|
|
relational(t, func(t *testing.T, db *query.DB) {
|
|
|
|
got, ok, err := orm.Select[typedUser](db, "users").
|
|
Where(query.HashExp{"id": "u1"}).
|
|
First(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("First: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatal("expected found=true")
|
|
}
|
|
if got.Email != "[email protected]" {
|
|
t.Errorf("got %+v", got)
|
|
}
|
|
|
|
_, ok, err = orm.Select[typedUser](db, "users").
|
|
Where(query.HashExp{"id": "nope"}).
|
|
First(context.Background())
|
|
if err != nil {
|
|
t.Errorf("unexpected err: %v", err)
|
|
}
|
|
if ok {
|
|
t.Error("expected found=false on empty")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestTypedChainingPassthrough(t *testing.T) {
|
|
relational(t, func(t *testing.T, db *query.DB) {
|
|
|
|
// Build up a query fluent-style, then reach into the underlying
|
|
// SelectQuery to apply a dbx method not mirrored on Typed, then
|
|
// terminate. Proves Typed.Query() is a working escape hatch.
|
|
tq := orm.Select[typedUser](db, "users").
|
|
Where(query.HashExp{"active": true}).
|
|
OrderBy("email")
|
|
|
|
// Escape hatch: apply a dbx-level method.
|
|
tq.Query().Limit(1)
|
|
|
|
got, err := tq.All(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("All: %v", err)
|
|
}
|
|
if len(got) != 1 {
|
|
t.Fatalf("want limit=1, got %d rows", len(got))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestNewTypedFromExternalBuilder(t *testing.T) {
|
|
// Prove NewTyped works with any *query.SelectQuery, not just those
|
|
// produced by orm.Select — this is the path that makes incremental
|
|
// migration trivial: existing code returning *dbx.SelectQuery (which
|
|
// IS *query.SelectQuery by identity) gets wrapped in Typed and flows
|
|
// through the typed terminators.
|
|
db := seedTypedDB(t)
|
|
sq := db.Select("*").From("users").Where(query.HashExp{"active": true}).OrderBy("email")
|
|
|
|
got, err := orm.NewTyped[typedUser](sq).All(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("All: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("want 2, got %d", len(got))
|
|
}
|
|
}
|