chore: import upstream snapshot with attribution
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
// Package sqlite provides a SQLite model.Model implementation.
|
||||
// Uses mattn/go-sqlite3 for broad compatibility.
|
||||
// Good for development, testing, and single-node production.
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"go-micro.dev/v6/model"
|
||||
)
|
||||
|
||||
type sqliteModel struct {
|
||||
db *sql.DB
|
||||
mu sync.RWMutex
|
||||
schemas map[string]*model.Schema
|
||||
types map[reflect.Type]*model.Schema
|
||||
}
|
||||
|
||||
// New creates a new SQLite model. DSN is the file path (e.g., "data.db" or ":memory:").
|
||||
func New(dsn string) model.Model {
|
||||
if dsn == "" {
|
||||
dsn = ":memory:"
|
||||
}
|
||||
db, err := sql.Open("sqlite3", dsn)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("model/sqlite: failed to open %q: %v", dsn, err))
|
||||
}
|
||||
_, _ = db.Exec("PRAGMA journal_mode=WAL")
|
||||
return &sqliteModel{
|
||||
db: db,
|
||||
schemas: make(map[string]*model.Schema),
|
||||
types: make(map[reflect.Type]*model.Schema),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *sqliteModel) Init(opts ...model.Option) error {
|
||||
return d.db.Ping()
|
||||
}
|
||||
|
||||
func (d *sqliteModel) Register(v interface{}, opts ...model.RegisterOption) error {
|
||||
schema := model.BuildSchema(v, opts...)
|
||||
t := model.ResolveType(v)
|
||||
|
||||
d.mu.Lock()
|
||||
d.schemas[schema.Table] = schema
|
||||
d.types[t] = schema
|
||||
d.mu.Unlock()
|
||||
|
||||
var cols []string
|
||||
for _, f := range schema.Fields {
|
||||
colType := goTypeToSQLite(f.Type)
|
||||
col := fmt.Sprintf("%q %s", f.Column, colType)
|
||||
if f.IsKey {
|
||||
col += " PRIMARY KEY"
|
||||
}
|
||||
cols = append(cols, col)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %q (%s)", schema.Table, strings.Join(cols, ", "))
|
||||
if _, err := d.db.Exec(query); err != nil {
|
||||
return fmt.Errorf("model/sqlite: create table: %w", err)
|
||||
}
|
||||
|
||||
for _, f := range schema.Fields {
|
||||
if f.Index && !f.IsKey {
|
||||
idx := fmt.Sprintf("CREATE INDEX IF NOT EXISTS %q ON %q (%q)",
|
||||
"idx_"+schema.Table+"_"+f.Column, schema.Table, f.Column)
|
||||
if _, err := d.db.Exec(idx); err != nil {
|
||||
return fmt.Errorf("model/sqlite: create index: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *sqliteModel) schema(v interface{}) (*model.Schema, error) {
|
||||
t := model.ResolveType(v)
|
||||
d.mu.RLock()
|
||||
s, ok := d.types[t]
|
||||
d.mu.RUnlock()
|
||||
if !ok {
|
||||
return nil, model.ErrNotRegistered
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (d *sqliteModel) Create(ctx context.Context, v interface{}) error {
|
||||
schema, err := d.schema(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fields := model.StructToMap(schema, v)
|
||||
cols, placeholders, values := buildInsert(schema, fields)
|
||||
query := fmt.Sprintf("INSERT INTO %q (%s) VALUES (%s)", schema.Table, cols, placeholders)
|
||||
_, err = d.db.ExecContext(ctx, query, values...)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint") || strings.Contains(err.Error(), "PRIMARY KEY") {
|
||||
return model.ErrDuplicateKey
|
||||
}
|
||||
return fmt.Errorf("model/sqlite: create: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *sqliteModel) Read(ctx context.Context, key string, v interface{}) error {
|
||||
schema, err := d.schema(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cols := columnList(schema)
|
||||
query := fmt.Sprintf("SELECT %s FROM %q WHERE %q = ?", cols, schema.Table, schema.Key)
|
||||
row := d.db.QueryRowContext(ctx, query, key)
|
||||
fields, err := scanRow(schema, row)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
model.MapToStruct(schema, fields, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *sqliteModel) Update(ctx context.Context, v interface{}) error {
|
||||
schema, err := d.schema(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fields := model.StructToMap(schema, v)
|
||||
key := model.KeyValue(schema, v)
|
||||
setClauses, values := buildUpdate(schema, fields)
|
||||
values = append(values, key)
|
||||
query := fmt.Sprintf("UPDATE %q SET %s WHERE %q = ?", schema.Table, setClauses, schema.Key)
|
||||
result, err := d.db.ExecContext(ctx, query, values...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("model/sqlite: update: %w", err)
|
||||
}
|
||||
n, _ := result.RowsAffected()
|
||||
if n == 0 {
|
||||
return model.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *sqliteModel) Delete(ctx context.Context, key string, v interface{}) error {
|
||||
schema, err := d.schema(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query := fmt.Sprintf("DELETE FROM %q WHERE %q = ?", schema.Table, schema.Key)
|
||||
result, err := d.db.ExecContext(ctx, query, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("model/sqlite: delete: %w", err)
|
||||
}
|
||||
n, _ := result.RowsAffected()
|
||||
if n == 0 {
|
||||
return model.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *sqliteModel) List(ctx context.Context, result interface{}, opts ...model.QueryOption) error {
|
||||
// result must be *[]*T
|
||||
rv := reflect.ValueOf(result)
|
||||
if rv.Kind() != reflect.Pointer || rv.Elem().Kind() != reflect.Slice {
|
||||
return fmt.Errorf("model/sqlite: result must be a pointer to a slice")
|
||||
}
|
||||
sliceVal := rv.Elem()
|
||||
elemType := sliceVal.Type().Elem()
|
||||
structType := elemType
|
||||
if structType.Kind() == reflect.Pointer {
|
||||
structType = structType.Elem()
|
||||
}
|
||||
|
||||
d.mu.RLock()
|
||||
schema, ok := d.types[structType]
|
||||
d.mu.RUnlock()
|
||||
if !ok {
|
||||
return model.ErrNotRegistered
|
||||
}
|
||||
|
||||
q := model.ApplyQueryOptions(opts...)
|
||||
cols := columnList(schema)
|
||||
|
||||
query := fmt.Sprintf("SELECT %s FROM %q", cols, schema.Table)
|
||||
var args []any
|
||||
|
||||
if len(q.Filters) > 0 {
|
||||
where, fArgs := buildWhere(q.Filters)
|
||||
query += " WHERE " + where
|
||||
args = append(args, fArgs...)
|
||||
}
|
||||
|
||||
if q.OrderBy != "" {
|
||||
dir := "ASC"
|
||||
if q.Desc {
|
||||
dir = "DESC"
|
||||
}
|
||||
query += fmt.Sprintf(" ORDER BY %q %s", q.OrderBy, dir)
|
||||
}
|
||||
|
||||
if q.Limit > 0 {
|
||||
query += fmt.Sprintf(" LIMIT %d", q.Limit)
|
||||
}
|
||||
if q.Offset > 0 {
|
||||
query += fmt.Sprintf(" OFFSET %d", q.Offset)
|
||||
}
|
||||
|
||||
rows, err := d.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("model/sqlite: list: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
fieldMaps, err := scanRows(schema, rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
results := reflect.MakeSlice(sliceVal.Type(), len(fieldMaps), len(fieldMaps))
|
||||
for i, fields := range fieldMaps {
|
||||
vp := reflect.New(structType)
|
||||
model.MapToStruct(schema, fields, vp.Interface())
|
||||
if elemType.Kind() == reflect.Pointer {
|
||||
results.Index(i).Set(vp)
|
||||
} else {
|
||||
results.Index(i).Set(vp.Elem())
|
||||
}
|
||||
}
|
||||
sliceVal.Set(results)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *sqliteModel) Count(ctx context.Context, v interface{}, opts ...model.QueryOption) (int64, error) {
|
||||
schema, err := d.schema(v)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
q := model.ApplyQueryOptions(opts...)
|
||||
|
||||
query := fmt.Sprintf("SELECT COUNT(*) FROM %q", schema.Table)
|
||||
var args []any
|
||||
|
||||
if len(q.Filters) > 0 {
|
||||
where, fArgs := buildWhere(q.Filters)
|
||||
query += " WHERE " + where
|
||||
args = append(args, fArgs...)
|
||||
}
|
||||
|
||||
var count int64
|
||||
err = d.db.QueryRowContext(ctx, query, args...).Scan(&count)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("model/sqlite: count: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (d *sqliteModel) Close() error {
|
||||
return d.db.Close()
|
||||
}
|
||||
|
||||
func (d *sqliteModel) String() string {
|
||||
return "sqlite"
|
||||
}
|
||||
|
||||
// SQL helpers
|
||||
|
||||
func goTypeToSQLite(t reflect.Type) string {
|
||||
switch t.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return "INTEGER"
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return "REAL"
|
||||
case reflect.Bool:
|
||||
return "INTEGER"
|
||||
default:
|
||||
return "TEXT"
|
||||
}
|
||||
}
|
||||
|
||||
func buildInsert(schema *model.Schema, fields map[string]any) (string, string, []any) {
|
||||
var cols []string
|
||||
var placeholders []string
|
||||
var values []any
|
||||
for _, f := range schema.Fields {
|
||||
if v, ok := fields[f.Column]; ok {
|
||||
cols = append(cols, fmt.Sprintf("%q", f.Column))
|
||||
placeholders = append(placeholders, "?")
|
||||
values = append(values, v)
|
||||
}
|
||||
}
|
||||
return strings.Join(cols, ", "), strings.Join(placeholders, ", "), values
|
||||
}
|
||||
|
||||
func buildUpdate(schema *model.Schema, fields map[string]any) (string, []any) {
|
||||
var setClauses []string
|
||||
var values []any
|
||||
for _, f := range schema.Fields {
|
||||
if f.IsKey {
|
||||
continue
|
||||
}
|
||||
if v, ok := fields[f.Column]; ok {
|
||||
setClauses = append(setClauses, fmt.Sprintf("%q = ?", f.Column))
|
||||
values = append(values, v)
|
||||
}
|
||||
}
|
||||
return strings.Join(setClauses, ", "), values
|
||||
}
|
||||
|
||||
func buildWhere(filters []model.Filter) (string, []any) {
|
||||
var clauses []string
|
||||
var args []any
|
||||
for _, f := range filters {
|
||||
clauses = append(clauses, fmt.Sprintf("%q %s ?", f.Field, f.Op))
|
||||
args = append(args, f.Value)
|
||||
}
|
||||
return strings.Join(clauses, " AND "), args
|
||||
}
|
||||
|
||||
func columnList(schema *model.Schema) string {
|
||||
var cols []string
|
||||
for _, f := range schema.Fields {
|
||||
cols = append(cols, fmt.Sprintf("%q", f.Column))
|
||||
}
|
||||
return strings.Join(cols, ", ")
|
||||
}
|
||||
|
||||
func scanRow(schema *model.Schema, row *sql.Row) (map[string]any, error) {
|
||||
ptrs := make([]any, len(schema.Fields))
|
||||
for i, f := range schema.Fields {
|
||||
ptrs[i] = newScanPtr(f.Type)
|
||||
}
|
||||
if err := row.Scan(ptrs...); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("model/sqlite: scan: %w", err)
|
||||
}
|
||||
result := make(map[string]any, len(schema.Fields))
|
||||
for i, f := range schema.Fields {
|
||||
result[f.Column] = derefScanPtr(ptrs[i], f.Type)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func scanRows(schema *model.Schema, rows *sql.Rows) ([]map[string]any, error) {
|
||||
var results []map[string]any
|
||||
for rows.Next() {
|
||||
ptrs := make([]any, len(schema.Fields))
|
||||
for i, f := range schema.Fields {
|
||||
ptrs[i] = newScanPtr(f.Type)
|
||||
}
|
||||
if err := rows.Scan(ptrs...); err != nil {
|
||||
return nil, fmt.Errorf("model/sqlite: scan row: %w", err)
|
||||
}
|
||||
row := make(map[string]any, len(schema.Fields))
|
||||
for i, f := range schema.Fields {
|
||||
row[f.Column] = derefScanPtr(ptrs[i], f.Type)
|
||||
}
|
||||
results = append(results, row)
|
||||
}
|
||||
return results, rows.Err()
|
||||
}
|
||||
|
||||
func newScanPtr(t reflect.Type) any {
|
||||
switch t.Kind() {
|
||||
case reflect.String:
|
||||
return new(string)
|
||||
case reflect.Int, reflect.Int64:
|
||||
return new(int64)
|
||||
case reflect.Int32:
|
||||
return new(int32)
|
||||
case reflect.Float64:
|
||||
return new(float64)
|
||||
case reflect.Float32:
|
||||
return new(float32)
|
||||
case reflect.Bool:
|
||||
return new(bool)
|
||||
default:
|
||||
return new(string)
|
||||
}
|
||||
}
|
||||
|
||||
func derefScanPtr(ptr any, t reflect.Type) any {
|
||||
rv := reflect.ValueOf(ptr).Elem()
|
||||
if rv.Type().ConvertibleTo(t) {
|
||||
return rv.Convert(t).Interface()
|
||||
}
|
||||
return rv.Interface()
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"go-micro.dev/v6/model"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id" model:"key"`
|
||||
Name string `json:"name" model:"index"`
|
||||
Email string `json:"email"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
|
||||
func setup(t *testing.T) model.Model {
|
||||
t.Helper()
|
||||
db := New(":memory:")
|
||||
if err := db.Register(&User{}); err != nil {
|
||||
t.Fatalf("register: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestCRUD(t *testing.T) {
|
||||
db := setup(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Create
|
||||
err := db.Create(ctx, &User{ID: "1", Name: "Alice", Email: "alice@test.com", Age: 30})
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
|
||||
// Read
|
||||
u := &User{}
|
||||
err = db.Read(ctx, "1", u)
|
||||
if err != nil {
|
||||
t.Fatalf("read: %v", err)
|
||||
}
|
||||
if u.Name != "Alice" {
|
||||
t.Errorf("expected Alice, got %s", u.Name)
|
||||
}
|
||||
if u.Age != 30 {
|
||||
t.Errorf("expected age 30, got %d", u.Age)
|
||||
}
|
||||
|
||||
// Update
|
||||
u.Name = "Alice Updated"
|
||||
u.Age = 31
|
||||
err = db.Update(ctx, u)
|
||||
if err != nil {
|
||||
t.Fatalf("update: %v", err)
|
||||
}
|
||||
|
||||
u2 := &User{}
|
||||
db.Read(ctx, "1", u2)
|
||||
if u2.Name != "Alice Updated" {
|
||||
t.Errorf("expected 'Alice Updated', got %s", u2.Name)
|
||||
}
|
||||
if u2.Age != 31 {
|
||||
t.Errorf("expected age 31, got %d", u2.Age)
|
||||
}
|
||||
|
||||
// Delete
|
||||
err = db.Delete(ctx, "1", &User{})
|
||||
if err != nil {
|
||||
t.Fatalf("delete: %v", err)
|
||||
}
|
||||
|
||||
err = db.Read(ctx, "1", &User{})
|
||||
if err != model.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicateKey(t *testing.T) {
|
||||
db := setup(t)
|
||||
ctx := context.Background()
|
||||
|
||||
db.Create(ctx, &User{ID: "1", Name: "Alice"})
|
||||
err := db.Create(ctx, &User{ID: "1", Name: "Bob"})
|
||||
if err != model.ErrDuplicateKey {
|
||||
t.Errorf("expected ErrDuplicateKey, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotFound(t *testing.T) {
|
||||
db := setup(t)
|
||||
ctx := context.Background()
|
||||
|
||||
err := db.Read(ctx, "nonexistent", &User{})
|
||||
if err != model.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
|
||||
err = db.Update(ctx, &User{ID: "nonexistent"})
|
||||
if err != model.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound on update, got %v", err)
|
||||
}
|
||||
|
||||
err = db.Delete(ctx, "nonexistent", &User{})
|
||||
if err != model.ErrNotFound {
|
||||
t.Errorf("expected ErrNotFound on delete, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListWithFilter(t *testing.T) {
|
||||
db := setup(t)
|
||||
ctx := context.Background()
|
||||
|
||||
db.Create(ctx, &User{ID: "1", Name: "Alice", Age: 30})
|
||||
db.Create(ctx, &User{ID: "2", Name: "Bob", Age: 25})
|
||||
db.Create(ctx, &User{ID: "3", Name: "Alice", Age: 35})
|
||||
|
||||
var results []*User
|
||||
err := db.List(ctx, &results, model.Where("name", "Alice"))
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 Alices, got %d", len(results))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListWithOrder(t *testing.T) {
|
||||
db := setup(t)
|
||||
ctx := context.Background()
|
||||
|
||||
db.Create(ctx, &User{ID: "1", Name: "Charlie", Age: 35})
|
||||
db.Create(ctx, &User{ID: "2", Name: "Alice", Age: 30})
|
||||
db.Create(ctx, &User{ID: "3", Name: "Bob", Age: 25})
|
||||
|
||||
var results []*User
|
||||
err := db.List(ctx, &results, model.OrderAsc("name"))
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
if len(results) != 3 {
|
||||
t.Fatalf("expected 3, got %d", len(results))
|
||||
}
|
||||
if results[0].Name != "Alice" {
|
||||
t.Errorf("expected Alice first, got %s", results[0].Name)
|
||||
}
|
||||
if results[2].Name != "Charlie" {
|
||||
t.Errorf("expected Charlie last, got %s", results[2].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListWithLimitOffset(t *testing.T) {
|
||||
db := setup(t)
|
||||
ctx := context.Background()
|
||||
|
||||
db.Create(ctx, &User{ID: "1", Name: "A", Age: 1})
|
||||
db.Create(ctx, &User{ID: "2", Name: "B", Age: 2})
|
||||
db.Create(ctx, &User{ID: "3", Name: "C", Age: 3})
|
||||
db.Create(ctx, &User{ID: "4", Name: "D", Age: 4})
|
||||
|
||||
var results []*User
|
||||
err := db.List(ctx, &results,
|
||||
model.OrderAsc("name"),
|
||||
model.Limit(2),
|
||||
model.Offset(1),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("expected 2, got %d", len(results))
|
||||
}
|
||||
if results[0].Name != "B" {
|
||||
t.Errorf("expected B, got %s", results[0].Name)
|
||||
}
|
||||
if results[1].Name != "C" {
|
||||
t.Errorf("expected C, got %s", results[1].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCount(t *testing.T) {
|
||||
db := setup(t)
|
||||
ctx := context.Background()
|
||||
|
||||
db.Create(ctx, &User{ID: "1", Name: "Alice", Age: 30})
|
||||
db.Create(ctx, &User{ID: "2", Name: "Bob", Age: 25})
|
||||
db.Create(ctx, &User{ID: "3", Name: "Alice", Age: 35})
|
||||
|
||||
count, err := db.Count(ctx, &User{})
|
||||
if err != nil {
|
||||
t.Fatalf("count: %v", err)
|
||||
}
|
||||
if count != 3 {
|
||||
t.Errorf("expected 3, got %d", count)
|
||||
}
|
||||
|
||||
count, err = db.Count(ctx, &User{}, model.Where("name", "Alice"))
|
||||
if err != nil {
|
||||
t.Fatalf("count with filter: %v", err)
|
||||
}
|
||||
if count != 2 {
|
||||
t.Errorf("expected 2, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhereOp(t *testing.T) {
|
||||
db := setup(t)
|
||||
ctx := context.Background()
|
||||
|
||||
db.Create(ctx, &User{ID: "1", Name: "Alice", Age: 30})
|
||||
db.Create(ctx, &User{ID: "2", Name: "Bob", Age: 25})
|
||||
db.Create(ctx, &User{ID: "3", Name: "Charlie", Age: 35})
|
||||
|
||||
var results []*User
|
||||
err := db.List(ctx, &results, model.WhereOp("age", ">", 28))
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
if len(results) != 2 {
|
||||
t.Errorf("expected 2 (age > 28), got %d", len(results))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user