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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:40:33 +08:00
commit e071084ebe
982 changed files with 160368 additions and 0 deletions
+163
View File
@@ -0,0 +1,163 @@
# Model Package
The `model` package provides a structured data storage interface with CRUD operations, query filtering, and multiple database backends.
Unlike the `store` package (which is a raw KV abstraction), `model` provides structured data access with schema awareness, WHERE queries, ordering, pagination, and indexes.
## Quick Start
```go
import (
"context"
"go-micro.dev/v5/model"
)
// Define your model with struct tags
type User struct {
ID string `json:"id" model:"key"`
Name string `json:"name" model:"index"`
Email string `json:"email"`
Age int `json:"age"`
}
// Create a model and register your type
db := model.NewModel()
db.Register(&User{})
ctx := context.Background()
// Create
db.Create(ctx, &User{ID: "1", Name: "Alice", Email: "alice@example.com", Age: 30})
// Read
user := &User{}
db.Read(ctx, "1", user)
fmt.Println(user.Name) // "Alice"
// Update
user.Name = "Alice Smith"
db.Update(ctx, user)
// Delete
db.Delete(ctx, "1", &User{})
```
## Struct Tags
| Tag | Description | Example |
|-----|-------------|---------|
| `model:"key"` | Primary key field | `ID string \`model:"key"\`` |
| `model:"index"` | Create an index on this field | `Name string \`model:"index"\`` |
| `json:"name"` | Column name in the database | `Name string \`json:"name"\`` |
If no `model:"key"` tag is found, the package defaults to a field with `json:"id"` or column name `id`.
## Querying
```go
// Filter by field value
var users []*User
db.List(ctx, &users, model.Where("name", "Alice"))
// Comparison operators
db.List(ctx, &users, model.WhereOp("age", ">", 25))
db.List(ctx, &users, model.WhereOp("name", "LIKE", "Ali%"))
// Ordering
db.List(ctx, &users, model.OrderAsc("name"))
db.List(ctx, &users, model.OrderDesc("age"))
// Pagination
db.List(ctx, &users, model.Limit(10), model.Offset(20))
// Combine
db.List(ctx, &users,
model.Where("status", "active"),
model.WhereOp("age", ">=", 18),
model.OrderDesc("created_at"),
model.Limit(25),
)
// Count
total, _ := db.Count(ctx, &User{})
active, _ := db.Count(ctx, &User{}, model.Where("status", "active"))
```
## Backends
### Memory (Development & Testing)
```go
import "go-micro.dev/v5/model"
db := model.NewModel()
```
In-memory storage. No persistence. Fast. Good for tests and prototyping.
### SQLite (Development & Single-Node Production)
```go
import "go-micro.dev/v5/model/sqlite"
db := sqlite.New("app.db") // File-based
db := sqlite.New(":memory:") // In-memory (testing)
```
Embedded SQL database. Zero external dependencies. Supports WHERE, indexes, ordering natively.
### Postgres (Production)
```go
import "go-micro.dev/v5/model/postgres"
db := postgres.New("postgres://user:pass@localhost/mydb?sslmode=disable")
```
Full PostgreSQL support. Best for production with rich query capabilities.
## Table Names
By default, the table name is the lowercase struct name + "s" (e.g., `User` → `users`). Override with `model.WithTable`:
```go
db.Register(&User{}, model.WithTable("app_users"))
```
## Model Interface
All backends implement the `model.Model` interface:
```go
type Model interface {
Init(...Option) error
Register(v interface{}, opts ...RegisterOption) error
Create(ctx context.Context, v interface{}) error
Read(ctx context.Context, key string, v interface{}) error
Update(ctx context.Context, v interface{}) error
Delete(ctx context.Context, key string, v interface{}) error
List(ctx context.Context, result interface{}, opts ...QueryOption) error
Count(ctx context.Context, v interface{}, opts ...QueryOption) (int64, error)
Close() error
String() string
}
```
## Model vs Store
| Feature | `store` | `model` |
|---------|---------|---------|
| Data format | Raw `[]byte` | Go structs |
| Queries | Key prefix/suffix only | WHERE, operators, LIKE |
| Ordering | None | ORDER BY field ASC/DESC |
| Pagination | Limit/Offset on keys | Limit/Offset on results |
| Indexes | None | Via `model:"index"` tag |
| Schema | None (schemaless KV) | Auto-created from struct |
| Backends | Memory, File, MySQL, Postgres, NATS | Memory, SQLite, Postgres |
| Use case | Config, sessions, cache | Application data, entities |
## Testing
```bash
go test ./model/...
```
+333
View File
@@ -0,0 +1,333 @@
package model
import (
"context"
"fmt"
"reflect"
"strings"
"sync"
)
type memoryModel struct {
mu sync.RWMutex
schemas map[string]*Schema
types map[reflect.Type]*Schema
tables map[string]map[string]map[string]any // table -> key -> fields
}
func newMemoryModel(opts ...Option) Model {
return &memoryModel{
schemas: make(map[string]*Schema),
types: make(map[reflect.Type]*Schema),
tables: make(map[string]map[string]map[string]any),
}
}
func (m *memoryModel) Init(opts ...Option) error {
return nil
}
func (m *memoryModel) Register(v interface{}, opts ...RegisterOption) error {
schema := BuildSchema(v, opts...)
t := ResolveType(v)
m.mu.Lock()
defer m.mu.Unlock()
m.schemas[schema.Table] = schema
m.types[t] = schema
if _, ok := m.tables[schema.Table]; !ok {
m.tables[schema.Table] = make(map[string]map[string]any)
}
return nil
}
func (m *memoryModel) schema(v interface{}) (*Schema, error) {
t := ResolveType(v)
m.mu.RLock()
s, ok := m.types[t]
m.mu.RUnlock()
if !ok {
return nil, ErrNotRegistered
}
return s, nil
}
func (m *memoryModel) Create(ctx context.Context, v interface{}) error {
schema, err := m.schema(v)
if err != nil {
return err
}
fields := StructToMap(schema, v)
key := KeyValue(schema, v)
if key == "" {
return fmt.Errorf("model: key field %q not set", schema.Key)
}
m.mu.Lock()
defer m.mu.Unlock()
tbl := m.tables[schema.Table]
if _, exists := tbl[key]; exists {
return ErrDuplicateKey
}
row := make(map[string]any, len(fields))
for k, v := range fields {
row[k] = v
}
tbl[key] = row
return nil
}
func (m *memoryModel) Read(ctx context.Context, key string, v interface{}) error {
schema, err := m.schema(v)
if err != nil {
return err
}
m.mu.RLock()
defer m.mu.RUnlock()
tbl := m.tables[schema.Table]
row, ok := tbl[key]
if !ok {
return ErrNotFound
}
MapToStruct(schema, row, v)
return nil
}
func (m *memoryModel) Update(ctx context.Context, v interface{}) error {
schema, err := m.schema(v)
if err != nil {
return err
}
fields := StructToMap(schema, v)
key := KeyValue(schema, v)
if key == "" {
return fmt.Errorf("model: key field %q not set", schema.Key)
}
m.mu.Lock()
defer m.mu.Unlock()
tbl := m.tables[schema.Table]
if _, ok := tbl[key]; !ok {
return ErrNotFound
}
row := make(map[string]any, len(fields))
for k, v := range fields {
row[k] = v
}
tbl[key] = row
return nil
}
func (m *memoryModel) Delete(ctx context.Context, key string, v interface{}) error {
schema, err := m.schema(v)
if err != nil {
return err
}
m.mu.Lock()
defer m.mu.Unlock()
tbl := m.tables[schema.Table]
if _, ok := tbl[key]; !ok {
return ErrNotFound
}
delete(tbl, key)
return nil
}
func (m *memoryModel) List(ctx context.Context, result interface{}, opts ...QueryOption) error {
// result must be *[]*T
rv := reflect.ValueOf(result)
if rv.Kind() != reflect.Pointer || rv.Elem().Kind() != reflect.Slice {
return fmt.Errorf("model: result must be a pointer to a slice")
}
sliceVal := rv.Elem()
elemType := sliceVal.Type().Elem() // *T
structType := elemType
if structType.Kind() == reflect.Pointer {
structType = structType.Elem()
}
m.mu.RLock()
s, ok := m.types[structType]
m.mu.RUnlock()
if !ok {
return ErrNotRegistered
}
q := ApplyQueryOptions(opts...)
m.mu.RLock()
tbl := m.tables[s.Table]
var rows []map[string]any
for _, row := range tbl {
if matchFilters(row, q.Filters) {
cp := make(map[string]any, len(row))
for k, v := range row {
cp[k] = v
}
rows = append(rows, cp)
}
}
m.mu.RUnlock()
if q.OrderBy != "" {
sortRows(rows, q.OrderBy, q.Desc)
}
if q.Offset > 0 && uint(len(rows)) > q.Offset {
rows = rows[q.Offset:]
} else if q.Offset > 0 {
rows = nil
}
if q.Limit > 0 && uint(len(rows)) > q.Limit {
rows = rows[:q.Limit]
}
results := reflect.MakeSlice(sliceVal.Type(), len(rows), len(rows))
for i, row := range rows {
vp := reflect.New(structType)
MapToStruct(s, row, 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 (m *memoryModel) Count(ctx context.Context, v interface{}, opts ...QueryOption) (int64, error) {
schema, err := m.schema(v)
if err != nil {
return 0, err
}
q := ApplyQueryOptions(opts...)
m.mu.RLock()
defer m.mu.RUnlock()
tbl := m.tables[schema.Table]
var count int64
for _, row := range tbl {
if matchFilters(row, q.Filters) {
count++
}
}
return count, nil
}
func (m *memoryModel) Close() error {
return nil
}
func (m *memoryModel) String() string {
return "memory"
}
// matchFilters returns true if the row satisfies all filters.
func matchFilters(row map[string]any, filters []Filter) bool {
for _, f := range filters {
val, ok := row[f.Field]
if !ok {
return false
}
if !compareValues(val, f.Op, f.Value) {
return false
}
}
return true
}
// compareValues compares two values with the given operator.
func compareValues(a any, op string, b any) bool {
switch op {
case "=":
return fmt.Sprint(a) == fmt.Sprint(b)
case "!=":
return fmt.Sprint(a) != fmt.Sprint(b)
case "LIKE":
pattern := fmt.Sprint(b)
val := fmt.Sprint(a)
if strings.HasPrefix(pattern, "%") && strings.HasSuffix(pattern, "%") {
return strings.Contains(val, pattern[1:len(pattern)-1])
}
if strings.HasPrefix(pattern, "%") {
return strings.HasSuffix(val, pattern[1:])
}
if strings.HasSuffix(pattern, "%") {
return strings.HasPrefix(val, pattern[:len(pattern)-1])
}
return val == pattern
case "<", ">", "<=", ">=":
return compareNumeric(a, op, b)
default:
return false
}
}
func compareNumeric(a any, op string, b any) bool {
af, aOk := toFloat64(a)
bf, bOk := toFloat64(b)
if !aOk || !bOk {
as, bs := fmt.Sprint(a), fmt.Sprint(b)
switch op {
case "<":
return as < bs
case ">":
return as > bs
case "<=":
return as <= bs
case ">=":
return as >= bs
}
return false
}
switch op {
case "<":
return af < bf
case ">":
return af > bf
case "<=":
return af <= bf
case ">=":
return af >= bf
}
return false
}
func toFloat64(v any) (float64, bool) {
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return float64(rv.Int()), true
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return float64(rv.Uint()), true
case reflect.Float32, reflect.Float64:
return rv.Float(), true
default:
return 0, false
}
}
func sortRows(rows []map[string]any, field string, desc bool) {
for i := 1; i < len(rows); i++ {
for j := i; j > 0; j-- {
a := fmt.Sprint(rows[j-1][field])
b := fmt.Sprint(rows[j][field])
shouldSwap := a > b
if desc {
shouldSwap = a < b
}
if shouldSwap {
rows[j-1], rows[j] = rows[j], rows[j-1]
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
// Package memory provides an in-memory model.Model implementation.
// This is the same as model.NewModel() but importable as a standalone package.
package memory
import (
"go-micro.dev/v6/model"
)
// New creates a new in-memory model.
func New(opts ...model.Option) model.Model {
return model.NewModel(opts...)
}
+215
View File
@@ -0,0 +1,215 @@
package memory
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()
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 TestList(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 all []*User
err := db.List(ctx, &all)
if err != nil {
t.Fatalf("list: %v", err)
}
if len(all) != 3 {
t.Errorf("expected 3, got %d", len(all))
}
}
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 with filter: %v", err)
}
if len(results) != 2 {
t.Errorf("expected 2 Alices, got %d", len(results))
}
}
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))
}
}
+86
View File
@@ -0,0 +1,86 @@
// Package model is an interface for structured data storage with schema awareness.
package model
import (
"context"
"errors"
)
var (
// ErrNotFound is returned when a record doesn't exist.
ErrNotFound = errors.New("not found")
// ErrDuplicateKey is returned when a record with the same key already exists.
ErrDuplicateKey = errors.New("duplicate key")
// ErrNotRegistered is returned when a table has not been registered.
ErrNotRegistered = errors.New("table not registered")
// DefaultModel is the default model.
DefaultModel Model = NewModel()
)
// Model is a structured data storage interface.
type Model interface {
// Init initializes the model.
Init(...Option) error
// Register registers a struct type as a table.
Register(v interface{}, opts ...RegisterOption) error
// Create inserts a new record. Returns ErrDuplicateKey if key exists.
Create(ctx context.Context, v interface{}) error
// Read retrieves a record by key into v. Returns ErrNotFound if missing.
Read(ctx context.Context, key string, v interface{}) error
// Update modifies an existing record. Returns ErrNotFound if missing.
Update(ctx context.Context, v interface{}) error
// Delete removes a record by key. v is a pointer to the struct type.
Delete(ctx context.Context, key string, v interface{}) error
// List retrieves records matching the query. result must be a pointer to a slice of struct pointers.
List(ctx context.Context, result interface{}, opts ...QueryOption) error
// Count returns the number of matching records. v is a pointer to the struct type.
Count(ctx context.Context, v interface{}, opts ...QueryOption) (int64, error)
// Close closes the model.
Close() error
// String returns the name of the implementation.
String() string
}
type Option func(*Options)
type RegisterOption func(*Schema)
// NewModel returns the default in-memory model.
func NewModel(opts ...Option) Model {
return newMemoryModel(opts...)
}
// Register registers a struct type with the default model.
func Register(v interface{}, opts ...RegisterOption) error {
return DefaultModel.Register(v, opts...)
}
// Create inserts a new record using the default model.
func Create(ctx context.Context, v interface{}) error {
return DefaultModel.Create(ctx, v)
}
// Read retrieves a record by key using the default model.
func Read(ctx context.Context, key string, v interface{}) error {
return DefaultModel.Read(ctx, key, v)
}
// Update modifies an existing record using the default model.
func Update(ctx context.Context, v interface{}) error {
return DefaultModel.Update(ctx, v)
}
// Delete removes a record by key using the default model.
func Delete(ctx context.Context, key string, v interface{}) error {
return DefaultModel.Delete(ctx, key, v)
}
// List retrieves records matching the query using the default model.
func List(ctx context.Context, result interface{}, opts ...QueryOption) error {
return DefaultModel.List(ctx, result, opts...)
}
// Count returns the number of matching records using the default model.
func Count(ctx context.Context, v interface{}, opts ...QueryOption) (int64, error) {
return DefaultModel.Count(ctx, v, opts...)
}
+138
View File
@@ -0,0 +1,138 @@
package model
import (
"testing"
)
type TestUser struct {
ID string `json:"id" model:"key"`
Name string `json:"name" model:"index"`
Email string `json:"email"`
Age int `json:"age"`
}
func TestBuildSchema(t *testing.T) {
schema := BuildSchema(TestUser{})
if schema.Table != "testusers" {
t.Errorf("expected table 'testusers', got %q", schema.Table)
}
if schema.Key != "id" {
t.Errorf("expected key 'id', got %q", schema.Key)
}
if len(schema.Fields) != 4 {
t.Fatalf("expected 4 fields, got %d", len(schema.Fields))
}
var keyField Field
var indexField Field
for _, f := range schema.Fields {
if f.IsKey {
keyField = f
}
if f.Index {
indexField = f
}
}
if keyField.Column != "id" {
t.Errorf("expected key column 'id', got %q", keyField.Column)
}
if indexField.Column != "name" {
t.Errorf("expected index column 'name', got %q", indexField.Column)
}
}
func TestBuildSchema_DefaultKey(t *testing.T) {
type Item struct {
ID string `json:"id"`
Name string `json:"name"`
}
schema := BuildSchema(Item{})
if schema.Key != "id" {
t.Errorf("expected default key 'id', got %q", schema.Key)
}
}
func TestBuildSchema_WithTable(t *testing.T) {
schema := BuildSchema(TestUser{}, WithTable("my_users"))
if schema.Table != "my_users" {
t.Errorf("expected table 'my_users', got %q", schema.Table)
}
}
func TestStructToMap(t *testing.T) {
schema := BuildSchema(TestUser{})
u := &TestUser{ID: "1", Name: "Alice", Email: "alice@example.com", Age: 30}
m := StructToMap(schema, u)
if m["id"] != "1" {
t.Errorf("expected id '1', got %v", m["id"])
}
if m["name"] != "Alice" {
t.Errorf("expected name 'Alice', got %v", m["name"])
}
if m["email"] != "alice@example.com" {
t.Errorf("expected email 'alice@example.com', got %v", m["email"])
}
if m["age"] != 30 {
t.Errorf("expected age 30, got %v", m["age"])
}
}
func TestMapToStruct(t *testing.T) {
schema := BuildSchema(TestUser{})
m := map[string]any{
"id": "1",
"name": "Bob",
"email": "bob@example.com",
"age": 25,
}
u := &TestUser{}
MapToStruct(schema, m, u)
if u.ID != "1" {
t.Errorf("expected ID '1', got %q", u.ID)
}
if u.Name != "Bob" {
t.Errorf("expected Name 'Bob', got %q", u.Name)
}
if u.Email != "bob@example.com" {
t.Errorf("expected Email 'bob@example.com', got %q", u.Email)
}
if u.Age != 25 {
t.Errorf("expected Age 25, got %d", u.Age)
}
}
func TestApplyQueryOptions(t *testing.T) {
q := ApplyQueryOptions(
Where("name", "Alice"),
WhereOp("age", ">", 20),
OrderDesc("name"),
Limit(10),
Offset(5),
)
if len(q.Filters) != 2 {
t.Fatalf("expected 2 filters, got %d", len(q.Filters))
}
if q.Filters[0].Field != "name" || q.Filters[0].Op != "=" || q.Filters[0].Value != "Alice" {
t.Errorf("unexpected filter 0: %+v", q.Filters[0])
}
if q.Filters[1].Field != "age" || q.Filters[1].Op != ">" {
t.Errorf("unexpected filter 1: %+v", q.Filters[1])
}
if q.OrderBy != "name" || !q.Desc {
t.Errorf("expected order by name desc, got %q desc=%v", q.OrderBy, q.Desc)
}
if q.Limit != 10 {
t.Errorf("expected limit 10, got %d", q.Limit)
}
if q.Offset != 5 {
t.Errorf("expected offset 5, got %d", q.Offset)
}
}
+30
View File
@@ -0,0 +1,30 @@
package model
// Options holds configuration for a Model.
type Options struct {
// DSN is the data source name / connection string.
DSN string
}
// WithDSN sets the data source name.
func WithDSN(dsn string) Option {
return func(o *Options) {
o.DSN = dsn
}
}
// NewOptions creates Options with defaults applied.
func NewOptions(opts ...Option) Options {
o := Options{}
for _, opt := range opts {
opt(&o)
}
return o
}
// WithTable overrides the auto-derived table name.
func WithTable(name string) RegisterOption {
return func(s *Schema) {
s.Table = name
}
}
+414
View File
@@ -0,0 +1,414 @@
// Package postgres provides a PostgreSQL model.Model implementation.
// Uses lib/pq driver. Best for production deployments with rich query support.
package postgres
import (
"context"
"database/sql"
"fmt"
"reflect"
"strings"
"sync"
_ "github.com/lib/pq"
"go-micro.dev/v6/model"
)
type postgresModel struct {
db *sql.DB
mu sync.RWMutex
schemas map[string]*model.Schema
types map[reflect.Type]*model.Schema
}
// New creates a new Postgres model. DSN is a connection string
// (e.g., "postgres://user:pass@localhost/dbname?sslmode=disable").
func New(dsn string) model.Model {
db, err := sql.Open("postgres", dsn)
if err != nil {
panic(fmt.Sprintf("model/postgres: failed to open: %v", err))
}
return &postgresModel{
db: db,
schemas: make(map[string]*model.Schema),
types: make(map[reflect.Type]*model.Schema),
}
}
func (d *postgresModel) Init(opts ...model.Option) error {
return d.db.Ping()
}
func (d *postgresModel) 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 := goTypeToPostgres(f.Type)
col := fmt.Sprintf("%s %s", quoteIdent(f.Column), colType)
if f.IsKey {
col += " PRIMARY KEY"
}
cols = append(cols, col)
}
query := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (%s)", quoteIdent(schema.Table), strings.Join(cols, ", "))
if _, err := d.db.Exec(query); err != nil {
return fmt.Errorf("model/postgres: create table: %w", err)
}
for _, f := range schema.Fields {
if f.Index && !f.IsKey {
idx := fmt.Sprintf("CREATE INDEX IF NOT EXISTS %s ON %s (%s)",
quoteIdent("idx_"+schema.Table+"_"+f.Column),
quoteIdent(schema.Table),
quoteIdent(f.Column))
if _, err := d.db.Exec(idx); err != nil {
return fmt.Errorf("model/postgres: create index: %w", err)
}
}
}
return nil
}
func (d *postgresModel) 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 *postgresModel) 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 %s (%s) VALUES (%s)", quoteIdent(schema.Table), cols, placeholders)
_, err = d.db.ExecContext(ctx, query, values...)
if err != nil {
if strings.Contains(err.Error(), "duplicate key") || strings.Contains(err.Error(), "unique constraint") {
return model.ErrDuplicateKey
}
return fmt.Errorf("model/postgres: create: %w", err)
}
return nil
}
func (d *postgresModel) 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 %s WHERE %s = $1", cols, quoteIdent(schema.Table), quoteIdent(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 *postgresModel) 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)
paramIdx := len(values)
query := fmt.Sprintf("UPDATE %s SET %s WHERE %s = $%d",
quoteIdent(schema.Table), setClauses, quoteIdent(schema.Key), paramIdx)
result, err := d.db.ExecContext(ctx, query, values...)
if err != nil {
return fmt.Errorf("model/postgres: update: %w", err)
}
n, _ := result.RowsAffected()
if n == 0 {
return model.ErrNotFound
}
return nil
}
func (d *postgresModel) Delete(ctx context.Context, key string, v interface{}) error {
schema, err := d.schema(v)
if err != nil {
return err
}
query := fmt.Sprintf("DELETE FROM %s WHERE %s = $1", quoteIdent(schema.Table), quoteIdent(schema.Key))
result, err := d.db.ExecContext(ctx, query, key)
if err != nil {
return fmt.Errorf("model/postgres: delete: %w", err)
}
n, _ := result.RowsAffected()
if n == 0 {
return model.ErrNotFound
}
return nil
}
func (d *postgresModel) List(ctx context.Context, result interface{}, opts ...model.QueryOption) error {
rv := reflect.ValueOf(result)
if rv.Kind() != reflect.Pointer || rv.Elem().Kind() != reflect.Slice {
return fmt.Errorf("model/postgres: 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 %s", cols, quoteIdent(schema.Table))
var args []any
paramN := 1
if len(q.Filters) > 0 {
where, fArgs, _ := buildWhere(q.Filters, paramN)
query += " WHERE " + where
args = append(args, fArgs...)
}
if q.OrderBy != "" {
dir := "ASC"
if q.Desc {
dir = "DESC"
}
query += fmt.Sprintf(" ORDER BY %s %s", quoteIdent(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/postgres: 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 *postgresModel) 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 %s", quoteIdent(schema.Table))
var args []any
paramN := 1
if len(q.Filters) > 0 {
where, fArgs, _ := buildWhere(q.Filters, paramN)
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/postgres: count: %w", err)
}
return count, nil
}
func (d *postgresModel) Close() error {
return d.db.Close()
}
func (d *postgresModel) String() string {
return "postgres"
}
// SQL helpers
func quoteIdent(s string) string {
return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
}
func goTypeToPostgres(t reflect.Type) string {
switch t.Kind() {
case reflect.Int, reflect.Int64:
return "BIGINT"
case reflect.Int8, reflect.Int16, reflect.Int32:
return "INTEGER"
case reflect.Uint, reflect.Uint64:
return "BIGINT"
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
return "INTEGER"
case reflect.Float32:
return "REAL"
case reflect.Float64:
return "DOUBLE PRECISION"
case reflect.Bool:
return "BOOLEAN"
default:
return "TEXT"
}
}
func buildInsert(schema *model.Schema, fields map[string]any) (string, string, []any) {
var cols []string
var placeholders []string
var values []any
i := 1
for _, f := range schema.Fields {
if v, ok := fields[f.Column]; ok {
cols = append(cols, quoteIdent(f.Column))
placeholders = append(placeholders, fmt.Sprintf("$%d", i))
values = append(values, v)
i++
}
}
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
i := 1
for _, f := range schema.Fields {
if f.IsKey {
continue
}
if v, ok := fields[f.Column]; ok {
setClauses = append(setClauses, fmt.Sprintf("%s = $%d", quoteIdent(f.Column), i))
values = append(values, v)
i++
}
}
return strings.Join(setClauses, ", "), values
}
func buildWhere(filters []model.Filter, startParam int) (string, []any, int) {
var clauses []string
var args []any
n := startParam
for _, f := range filters {
clauses = append(clauses, fmt.Sprintf("%s %s $%d", quoteIdent(f.Field), f.Op, n))
args = append(args, f.Value)
n++
}
return strings.Join(clauses, " AND "), args, n
}
func columnList(schema *model.Schema) string {
var cols []string
for _, f := range schema.Fields {
cols = append(cols, quoteIdent(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/postgres: 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/postgres: 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()
}
+73
View File
@@ -0,0 +1,73 @@
package model
// QueryOptions configures a List or Count operation.
type QueryOptions struct {
Filters []Filter
OrderBy string
Desc bool
Limit uint
Offset uint
}
// Filter represents a field-level query condition.
type Filter struct {
Field string // Column name
Op string // Operator: =, !=, <, >, <=, >=, LIKE
Value any // Comparison value
}
// QueryOption sets values in QueryOptions.
type QueryOption func(*QueryOptions)
// ApplyQueryOptions applies a set of QueryOptions and returns the result.
func ApplyQueryOptions(opts ...QueryOption) QueryOptions {
q := QueryOptions{}
for _, o := range opts {
o(&q)
}
return q
}
// Where adds an equality filter: field = value.
func Where(field string, value any) QueryOption {
return func(q *QueryOptions) {
q.Filters = append(q.Filters, Filter{Field: field, Op: "=", Value: value})
}
}
// WhereOp adds a filter with a custom operator (=, !=, <, >, <=, >=, LIKE).
func WhereOp(field, op string, value any) QueryOption {
return func(q *QueryOptions) {
q.Filters = append(q.Filters, Filter{Field: field, Op: op, Value: value})
}
}
// OrderAsc orders results by field ascending.
func OrderAsc(field string) QueryOption {
return func(q *QueryOptions) {
q.OrderBy = field
q.Desc = false
}
}
// OrderDesc orders results by field descending.
func OrderDesc(field string) QueryOption {
return func(q *QueryOptions) {
q.OrderBy = field
q.Desc = true
}
}
// Limit limits the number of returned records.
func Limit(n uint) QueryOption {
return func(q *QueryOptions) {
q.Limit = n
}
}
// Offset skips the first n records (for pagination).
func Offset(n uint) QueryOption {
return func(q *QueryOptions) {
q.Offset = n
}
}
+162
View File
@@ -0,0 +1,162 @@
package model
import (
"fmt"
"reflect"
"strings"
)
// Schema describes a model's storage layout, derived from struct tags.
type Schema struct {
// Table name in the database.
Table string
// Key is the name of the primary key field.
Key string
// Fields maps Go field names to their column metadata.
Fields []Field
}
// Field describes a single field in the schema.
type Field struct {
// Name is the Go struct field name.
Name string
// Column is the database column name (from json tag or lowercased name).
Column string
// Type is the Go reflect type.
Type reflect.Type
// IsKey indicates this is the primary key field.
IsKey bool
// Index indicates this field should be indexed.
Index bool
}
// BuildSchema extracts a Schema from a struct type using reflection.
func BuildSchema(v interface{}, opts ...RegisterOption) *Schema {
t := reflect.TypeOf(v)
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
schema := &Schema{
Table: strings.ToLower(t.Name()) + "s",
}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if !f.IsExported() {
continue
}
field := Field{
Name: f.Name,
Type: f.Type,
}
// Column name: use json tag if present, else lowercase field name
if tag := f.Tag.Get("json"); tag != "" {
parts := strings.Split(tag, ",")
if parts[0] != "" && parts[0] != "-" {
field.Column = parts[0]
}
}
if field.Column == "" {
field.Column = strings.ToLower(f.Name)
}
// Check model tag
if tag := f.Tag.Get("model"); tag != "" {
for _, opt := range strings.Split(tag, ",") {
switch opt {
case "key":
field.IsKey = true
schema.Key = field.Column
case "index":
field.Index = true
}
}
}
schema.Fields = append(schema.Fields, field)
}
if schema.Key == "" {
// Default to "id" if no key tag found
for i := range schema.Fields {
if schema.Fields[i].Column == "id" {
schema.Fields[i].IsKey = true
schema.Key = "id"
break
}
}
}
for _, o := range opts {
o(schema)
}
return schema
}
// StructToMap converts a struct pointer to a map of column name → value.
func StructToMap(schema *Schema, v interface{}) map[string]any {
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Pointer {
rv = rv.Elem()
}
fields := make(map[string]any, len(schema.Fields))
for _, f := range schema.Fields {
fv := rv.FieldByName(f.Name)
if fv.IsValid() {
fields[f.Column] = fv.Interface()
}
}
return fields
}
// MapToStruct fills a struct pointer from a map of column name → value.
func MapToStruct(schema *Schema, fields map[string]any, v interface{}) {
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Pointer {
rv = rv.Elem()
}
for _, f := range schema.Fields {
val, ok := fields[f.Column]
if !ok {
continue
}
fv := rv.FieldByName(f.Name)
if !fv.IsValid() || !fv.CanSet() {
continue
}
rval := reflect.ValueOf(val)
if rval.Type().AssignableTo(fv.Type()) {
fv.Set(rval)
} else if rval.Type().ConvertibleTo(fv.Type()) {
fv.Set(rval.Convert(fv.Type()))
}
}
}
// NewFromSchema creates a new zero-value struct pointer for the given schema's original type.
func NewFromSchema(schema *Schema, rtype reflect.Type) interface{} {
return reflect.New(rtype).Interface()
}
// KeyValue extracts the key value from a struct using the schema.
func KeyValue(schema *Schema, v interface{}) string {
fields := StructToMap(schema, v)
key, ok := fields[schema.Key]
if !ok {
return ""
}
return fmt.Sprint(key)
}
// ResolveType returns the struct reflect.Type from a value (handles pointers and slices).
func ResolveType(v interface{}) reflect.Type {
t := reflect.TypeOf(v)
for t.Kind() == reflect.Pointer || t.Kind() == reflect.Slice {
t = t.Elem()
}
return t
}
+396
View File
@@ -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()
}
+221
View File
@@ -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))
}
}