diff --git a/queries/README.md b/queries/README.md index 29bcdf2e..a0f35d7c 100644 --- a/queries/README.md +++ b/queries/README.md @@ -22,12 +22,12 @@ Databases must always be opened using `drivers.Open(ctx, driverName, dsn)`. * [Interfaces](./docs/interfaces.md) * [Querying Objects](./docs/querying.md) * [QuerySet](./docs/queryset/queryset.md) - * [Writing Queries](./docs/queryset/writing_queries.md) (WIP) -* [Relations & Joins](./docs/relations/relations.md) (WIP) + * [Writing Queries](./docs/queryset/writing_queries.md) +* [Relations & Joins](./docs/relations/relations.md) * [Expressions](./docs/expressions/expressions.md) * [Lookups](./docs/expressions/lookups.md) * [Case Expressions](./docs/expressions/cases.md) -* [Advanced: Virtual Fields](./docs/virtual_fields.md) (WIP) +* [Advanced: Virtual Fields](./docs/virtual_fields.md) --- @@ -55,9 +55,9 @@ Continue with [Getting Started](./docs/getting_started.md)โ€ฆ We try to support as many features as possible, but some stuff is either not supported, implemented or tested yet. -## TODO: +### TODO: -* Implement eficient prefetching of multiple- relations +* Implement efficient prefetching of multiple- relations ### Tested Databases diff --git a/queries/docs/getting_started.md b/queries/docs/getting_started.md index d6765308..d0729d40 100644 --- a/queries/docs/getting_started.md +++ b/queries/docs/getting_started.md @@ -26,11 +26,11 @@ The following imports will be used throughout the examples: ```go import ( - "github.com/Nigel2392/go-django/queries/queries" - "github.com/Nigel2392/go-django/queries/queries/expr" - "github.com/Nigel2392/go-django/queries/queries/fields" - "github.com/Nigel2392/go-django/queries/queries/models" - "github.com/Nigel2392/go-django/queries/queries/qerr" + "github.com/Nigel2392/go-django/queries/src" + "github.com/Nigel2392/go-django/queries/src/expr" + "github.com/Nigel2392/go-django/queries/src/fields" + "github.com/Nigel2392/go-django/queries/src/models" + "github.com/Nigel2392/go-django/queries/src/drivers/errors" "github.com/Nigel2392/go-django/src/core/attrs" ) ``` @@ -46,7 +46,6 @@ For this example, we will use SQLite, but you can use any database that is suppo To setup the database, we need to create a `sql.DB` object, and register it in Go-Django's settings. ```go - func main() { var db, err = drivers.Open("sqlite3", "./db.sqlite3") if err != nil { @@ -55,7 +54,7 @@ func main() { var app = django.App( django.Configure(map[string]interface{}{ - django.APPVAR_DATABASE: db, + django.APPVAR_DATABASE: db, // ... }), // ... diff --git a/queries/docs/queryset/writing_queries.md b/queries/docs/queryset/writing_queries.md index e69de29b..2702c550 100644 --- a/queries/docs/queryset/writing_queries.md +++ b/queries/docs/queryset/writing_queries.md @@ -0,0 +1,911 @@ +# Writing Queries + +This guide covers practical techniques for writing queries using the `go-django-queries` package. + +Building on the [QuerySet Reference](./queryset.md), this document explores patterns for constructing database queries based on actual usage patterns from the test suite. + +--- + +## ๐Ÿ—๏ธ Query Construction Patterns + +### Basic Query Building + +Start with simple queries and build complexity incrementally: + +```go +// Start with a base queryset +qs := queries.GetQuerySet(&User{}) + +// Add filters progressively +qs = qs.Filter("IsActive", true) +qs = qs.Filter("CreatedAt__gte", time.Now().AddDate(0, -1, 0)) + +// Add ordering and limits +qs = qs.OrderBy("-CreatedAt") +qs = qs.Limit(10) + +// Execute the query +users, err := qs.All() +``` + +### Method Chaining + +Use method chaining for more readable query construction: + +```go +users, err := queries.GetQuerySet(&User{}). + Filter("IsActive", true). + Filter("Email__contains", "@example.com"). + OrderBy("-CreatedAt"). + Limit(20). + All() +``` + +### Conditional Query Building + +Build queries dynamically based on conditions: + +```go +func GetUsersWithFilters(country string, minAge int, orderBy string) ([]*User, error) { + qs := queries.GetQuerySet(&User{}) + + if country != "" { + qs = qs.Filter("Country", country) + } + + if minAge > 0 { + qs = qs.Filter("Age__gte", minAge) + } + + if orderBy != "" { + qs = qs.OrderBy(orderBy) + } + + return qs.All() +} +``` + +--- + +## ๐Ÿ” Expression-Based Filtering + +### Using Q Expressions + +Use `expr.Q()` for complex queries: + +```go +// Simple Q expression +users, err := queries.GetQuerySet(&User{}). + Filter(expr.Q("IsActive", true)). + All() + +// Complex filtering with nested conditions +users, err := queries.GetQuerySet(&User{}). + Filter(expr.Q("Age__gte", 18)). + Filter(expr.Q("Country", "US")). + All() +``` + +### Logical Operators + +Use explicit logical operators for complex conditions: + +```go +// OR conditions - use explicit expr.Or() +users, err := queries.GetQuerySet(&User{}). + Filter(expr.Or( + expr.Q("Country", "US"), + expr.Q("Country", "CA"), + )). + All() + +// AND conditions - use explicit expr.And() +users, err := queries.GetQuerySet(&User{}). + Filter(expr.And( + expr.Q("IsActive", true), + expr.Q("Age__gte", 18), + )). + All() + +// Complex nested conditions +users, err := queries.GetQuerySet(&User{}). + Filter(expr.And( + expr.Q("IsActive", true), + expr.Or( + expr.Q("Role", "admin"), + expr.Q("Role", "moderator"), + ), + )). + All() +``` + +### Field Expressions + +Use field expressions for comparisons between fields: + +```go +// Compare fields using expr.Expr +users, err := queries.GetQuerySet(&User{}). + Filter(expr.Expr("CreatedAt", expr.LOOKUP_LT, "UpdatedAt")). + All() + +// Bitwise operations +pages, err := queries.GetQuerySet(&Page{}). + Filter(expr.Expr("StatusFlags", expr.LOOKUP_BITAND, StatusFlagPublished)). + All() +``` + +--- + +## ๐Ÿ“Š Aggregations and Annotations + +### Basic Aggregations + +Use aggregation functions for calculations: + +```go +// Count records +count, err := queries.GetQuerySet(&User{}). + Filter("IsActive", true). + Count() + +// Aggregate specific fields +avgAge, err := queries.GetQuerySet(&User{}). + Filter("IsActive", true). + Aggregate("Age", "avg") +``` + +### Annotations + +Add computed fields to your queries: + +```go +// Annotate with count +users, err := queries.GetQuerySet(&User{}). + Annotate("PostCount", expr.Count("ID")). + All() + +// Access annotated fields +for _, user := range users { + fmt.Printf("User: %s, Posts: %d\n", user.Name, user.PostCount) +} +``` + +### Case Expressions + +Use case expressions for conditional logic: + +```go +// Simple case expression +users, err := queries.GetQuerySet(&User{}). + Annotate("UserType", + expr.Case( + expr.When(expr.Q("IsAdmin", true), "Admin"), + expr.When(expr.Q("IsModerator", true), "Moderator"), + expr.Value("Regular"), + ), + ). + All() +``` + +--- + +## ๐Ÿ”ง Advanced Query Techniques + +### Raw SQL Expressions + +Use raw SQL for complex operations: + +```go +// Raw SQL expression +users, err := queries.GetQuerySet(&User{}). + Filter(expr.Raw("AGE > ?", 18)). + All() + +// Raw field expression +users, err := queries.GetQuerySet(&User{}). + Update(&User{}, + expr.F("![UpdatedAt] = CURRENT_TIMESTAMP"), + ) +``` + +### Subqueries + +Use subqueries for complex filtering: + +```go +// Subquery for filtering +activeUsers, err := queries.GetQuerySet(&User{}). + Filter("ID", queries.Subquery( + queries.GetQuerySet(&Session{}). + Filter("IsActive", true). + Values("UserID"), + )). + All() +``` + +### Exists Queries + +Check for existence of related records: + +```go +// Users with posts +usersWithPosts, err := queries.GetQuerySet(&User{}). + Filter(expr.Exists( + queries.GetQuerySet(&Post{}). + Filter("Author", expr.OuterRef("ID")), + )). + All() +``` + +--- + +## ๐ŸŽฏ Query Optimization + +### Select Related + +Always select related data to avoid N+1 queries: + +```go +// Load related data in one query +posts, err := queries.GetQuerySet(&Post{}). + Select("*", "Author.*", "Category.*"). + All() +``` + +### Prefetch Related + +Use prefetch for reverse relationships: + +```go +// Prefetch related objects +users, err := queries.GetQuerySet(&User{}). + Prefetch("Posts"). + All() +``` + +### Distinct + +Remove duplicate results: + +```go +// Get unique users who have posts +users, err := queries.GetQuerySet(&User{}). + Filter(expr.Q("Posts__ID__isnull", false)). + Distinct(). + All() +``` + +### Indexes and Database Hints + +Use database-specific optimizations: + +```go +// Use index hints (database-specific) +users, err := queries.GetQuerySet(&User{}). + Filter("Email", "user@example.com"). + Extra("USE INDEX (email_idx)"). + All() +``` + +--- + +## ๐Ÿงช Testing Strategies + +### Test Data Setup + +Create reusable test data: + +```go +func setupTestData(t *testing.T) (*User, []*Post) { + user := &User{Name: "Test User", Email: "test@example.com"} + createdUser, err := queries.GetQuerySet(user).Create(user) + require.NoError(t, err) + + posts := []*Post{ + {Title: "Post 1", Author: createdUser}, + {Title: "Post 2", Author: createdUser}, + } + createdPosts, err := queries.GetQuerySet(&Post{}).BulkCreate(posts) + require.NoError(t, err) + + return createdUser, createdPosts +} +``` + +### Query Testing + +Test complex queries thoroughly: + +```go +func TestComplexQuery(t *testing.T) { + user, posts := setupTestData(t) + + // Test complex filtering + results, err := queries.GetQuerySet(&Post{}). + Filter(expr.And( + expr.Q("Author", user.ID), + expr.Q("Title__contains", "Post"), + )). + OrderBy("Title"). + All() + + require.NoError(t, err) + assert.Len(t, results, 2) + assert.Equal(t, "Post 1", results[0].Title) +} +``` + +### Performance Testing + +Test query performance: + +```go +func BenchmarkComplexQuery(b *testing.B) { + setupTestData(b) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := queries.GetQuerySet(&Post{}). + Select("*", "Author.*"). + Filter("Published", true). + OrderBy("-CreatedAt"). + Limit(100). + All() + if err != nil { + b.Fatal(err) + } + } +} +``` + +--- + +## ๐Ÿ“š Real-World Examples + +### User Management + +```go +// Get active users with recent activity +activeUsers, err := queries.GetQuerySet(&User{}). + Filter(expr.And( + expr.Q("IsActive", true), + expr.Q("LastLogin__gte", time.Now().AddDate(0, 0, -30)), + )). + OrderBy("-LastLogin"). + Limit(100). + All() +``` + +### Content Management + +```go +// Get published posts by category +posts, err := queries.GetQuerySet(&Post{}). + Filter(expr.And( + expr.Q("Published", true), + expr.Q("Category", category), + )). + Select("*", "Author.*"). + OrderBy("-PublishedAt"). + All() +``` + +### Analytics + +```go +// User engagement analytics +stats, err := queries.GetQuerySet(&User{}). + Annotate("PostCount", expr.Count("Posts")). + Annotate("CommentCount", expr.Count("Comments")). + Filter(expr.Q("PostCount__gt", 0)). + OrderBy("-PostCount"). + All() +``` + +This guide provides practical patterns for writing queries in the go-django-queries package, all based on actual usage patterns from the test suite. + qs := queries.GetQuerySet(&User{}).Select("*", "Profile.*") + + if country != "" { + qs = qs.Filter("Profile.Country", country) + } + + if minAge > 0 { + qs = qs.Filter("Profile.Age__gte", minAge) + } + + if orderBy != "" { + qs = qs.OrderBy(orderBy) + } else { + qs = qs.OrderBy("-CreatedAt") + } + + return qs.All() +} +``` + +--- + +## ๐Ÿ” Advanced Filtering + +### Complex Conditions with Expressions + +Use expressions for complex filtering conditions: + +```go +// Using Q expressions for complex conditions +users, err := queries.GetQuerySet(&User{}). + Filter(expr.Q("Age__gte", 18).And(expr.Q("Country", "US").Or(expr.Q("Country", "CA")))). + All() + +// Using raw expressions +users, err := queries.GetQuerySet(&User{}). + Filter(expr.Raw("UPPER(name) LIKE ?", "%JOHN%")). + All() +``` + +### Multiple Filter Conditions + +Combine multiple filters effectively: + +```go +// All conditions must be true (AND) +users, err := queries.GetQuerySet(&User{}). + Filter("IsActive", true). + Filter("Age__gte", 18). + Filter("Country__in", []string{"US", "CA", "UK"}). + All() + +// Using OR conditions +users, err := queries.GetQuerySet(&User{}). + Filter(expr.Q("Country", "US").Or(expr.Q("Age__gte", 21))). + All() +``` + +### Filtering by Related Models + +Filter by fields in related models: + +```go +// Filter by foreign key relation +todos, err := queries.GetQuerySet(&Todo{}). + Filter("User.IsActive", true). + Filter("User.Profile.Country", "US"). + All() + +// Filter by reverse relation +users, err := queries.GetQuerySet(&User{}). + Filter("TodoSet.Done", false). + Filter("TodoSet.Priority", "high"). + All() +``` + +### Date and Time Filtering + +Work with date and time fields: + +```go +// Date range queries +now := time.Now() +lastMonth := now.AddDate(0, -1, 0) + +todos, err := queries.GetQuerySet(&Todo{}). + Filter("CreatedAt__gte", lastMonth). + Filter("CreatedAt__lte", now). + All() + +// Date component queries +todos, err := queries.GetQuerySet(&Todo{}). + Filter("CreatedAt__year", 2023). + Filter("CreatedAt__month", 12). + All() +``` + +--- + +## ๐Ÿ“Š Aggregations and Annotations + +### Basic Aggregations + +Use aggregations to compute summary statistics: + +```go +// Count total users +count, err := queries.GetQuerySet(&User{}).Count() + +// Aggregate multiple values +stats, err := queries.GetQuerySet(&Todo{}). + Aggregate(map[string]expr.Expression{ + "total_count": expr.Count("ID"), + "completed_count": expr.Count("ID", expr.Q("Done", true)), + "avg_priority": expr.Avg("Priority"), + "max_created_at": expr.Max("CreatedAt"), + }) +``` + +### Annotations + +Add computed fields to your results: + +```go +// Annotate with calculated fields +users, err := queries.GetQuerySet(&User{}). + Annotate("TodoCount", expr.Count("TodoSet.ID")). + Annotate("CompletedTodos", expr.Count("TodoSet.ID", expr.Q("TodoSet.Done", true))). + Annotate("CompletionRate", expr.Div( + expr.Count("TodoSet.ID", expr.Q("TodoSet.Done", true)), + expr.Count("TodoSet.ID"), + )). + All() +``` + +### Complex Annotations + +Create sophisticated calculated fields: + +```go +// Complex annotation with case expressions +users, err := queries.GetQuerySet(&User{}). + Annotate("UserType", expr.Case( + expr.When(expr.Q("TodoSet__count__gte", 10), "Power User"), + expr.When(expr.Q("TodoSet__count__gte", 5), "Regular User"), + expr.Default("New User"), + )). + All() +``` + +--- + +## ๐ŸŽฏ Optimization Techniques + +### Selective Field Loading + +Load only the fields you need: + +```go +// Load specific fields only +users, err := queries.GetQuerySet(&User{}). + Select("ID", "Name", "Email"). + All() + +// Load related fields selectively +todos, err := queries.GetQuerySet(&Todo{}). + Select("*", "User.Name", "User.Email"). + All() +``` + +### Efficient Relation Loading + +Optimize relation loading to avoid N+1 queries: + +```go +// Good: Load relations in the initial query +todos, err := queries.GetQuerySet(&Todo{}). + Select("*", "User.*", "Category.*"). + All() + +// Bad: This causes N+1 queries +todos, err := queries.GetQuerySet(&Todo{}).All() +for _, row := range todos { + todo := row.Value() + // Additional query for each todo + user, _ := queries.GetObject(&User{}, todo.UserID) +} +``` + +### Bulk Operations + +Use bulk operations for better performance: + +```go +// Bulk create +newTodos := []*Todo{ + {Title: "Todo 1", UserID: 1}, + {Title: "Todo 2", UserID: 2}, + {Title: "Todo 3", UserID: 3}, +} +createdTodos, err := queries.GetQuerySet(&Todo{}). + BulkCreate(newTodos) + +// Bulk update +err = queries.GetQuerySet(&Todo{}). + Filter("Done", false). + BulkUpdate(newTodos, expr.Named("Done", true)) +``` + +--- + +## ๐Ÿ”„ Scopes and Reusable Queries + +### Defining Scopes + +Create reusable query scopes: + +```go +// Define common query scopes +func ActiveUsersScope(qs queries.QuerySet[*User], internals *queries.QuerySetInternals) queries.QuerySet[*User] { + return qs.Filter("IsActive", true) +} + +func RecentUsersScope(qs queries.QuerySet[*User], internals *queries.QuerySetInternals) queries.QuerySet[*User] { + return qs.Filter("CreatedAt__gte", time.Now().AddDate(0, -1, 0)) +} + +// Use scopes in queries +users, err := queries.GetQuerySet(&User{}). + Scope(ActiveUsersScope, RecentUsersScope). + OrderBy("-CreatedAt"). + All() +``` + +### Query Builders + +Create query builder functions for complex queries: + +```go +func BuildUserQuery(filters map[string]interface{}) queries.QuerySet[*User] { + qs := queries.GetQuerySet(&User{}).Select("*", "Profile.*") + + if country, ok := filters["country"]; ok { + qs = qs.Filter("Profile.Country", country) + } + + if minAge, ok := filters["min_age"]; ok { + qs = qs.Filter("Profile.Age__gte", minAge) + } + + if search, ok := filters["search"]; ok { + qs = qs.Filter(expr.Q("Name__icontains", search).Or( + expr.Q("Email__icontains", search), + )) + } + + return qs +} + +// Use the builder +users, err := BuildUserQuery(map[string]interface{}{ + "country": "US", + "min_age": 18, + "search": "john", +}).All() +``` + +--- + +## ๐Ÿ”ง Raw SQL and Custom Queries + +### Raw SQL Queries + +Use raw SQL when needed: + +```go +// Raw SQL query +rows, err := queries.GetQuerySet(&User{}). + Raw("SELECT * FROM users WHERE created_at > ? AND country = ?", + time.Now().AddDate(0, -1, 0), "US") + +// Raw SQL with result scanning +type UserStats struct { + Country string + Count int +} + +var stats []UserStats +rows, err := queries.GetQuerySet(&User{}). + Raw("SELECT country, COUNT(*) as count FROM users GROUP BY country") +if err != nil { + return err +} +defer rows.Close() + +for rows.Next() { + var stat UserStats + err := rows.Scan(&stat.Country, &stat.Count) + if err != nil { + return err + } + stats = append(stats, stat) +} +``` + +### Custom Expressions + +Create custom expressions for complex calculations: + +```go +// Custom expression for distance calculation +type DistanceExpression struct { + lat1, lon1, lat2, lon2 float64 +} + +func (d *DistanceExpression) SQL(sb *strings.Builder) []interface{} { + sb.WriteString("6371 * acos(cos(radians(?)) * cos(radians(lat)) * cos(radians(lon) - radians(?)) + sin(radians(?)) * sin(radians(lat)))") + return []interface{}{d.lat1, d.lon1, d.lat1} +} + +// Use in queries +locations, err := queries.GetQuerySet(&Location{}). + Annotate("Distance", &DistanceExpression{ + lat1: userLat, lon1: userLon, + lat2: 0, lon2: 0, // These will be filled from database + }). + Filter("Distance__lte", 50). // Within 50km + OrderBy("Distance"). + All() +``` + +--- + +## ๐Ÿ”€ Subqueries and Exists + +### Subqueries + +Use subqueries for complex filtering: + +```go +// Subquery to find users with incomplete todos +incompleteSubquery := queries.GetQuerySet(&Todo{}). + Filter("Done", false). + Filter("User", expr.OuterRef("ID")). + ValuesList("ID") + +users, err := queries.GetQuerySet(&User{}). + Filter("ID__in", incompleteSubquery). + All() +``` + +### Exists Queries + +Use exists for efficient filtering: + +```go +// Find users who have at least one incomplete todo +users, err := queries.GetQuerySet(&User{}). + Filter(expr.Exists( + queries.GetQuerySet(&Todo{}). + Filter("User", expr.OuterRef("ID")). + Filter("Done", false), + )). + All() +``` + +--- + +## ๐Ÿ“ˆ Performance Monitoring + +### Query Analysis + +Monitor and analyze query performance: + +```go +// Get query information +qs := queries.GetQuerySet(&User{}). + Select("*", "Profile.*"). + Filter("IsActive", true) + +users, err := qs.All() +if err != nil { + return err +} + +// Access the latest query information +queryInfo := qs.LatestQuery() +log.Printf("SQL: %s", queryInfo.SQL()) +log.Printf("Args: %v", queryInfo.Args()) +``` + +### Database Indexes + +Ensure proper indexes for query performance: + +```sql +-- Create indexes for commonly filtered/ordered fields +CREATE INDEX idx_users_is_active ON users(is_active); +CREATE INDEX idx_users_created_at ON users(created_at); +CREATE INDEX idx_todos_user_id_done ON todos(user_id, done); +CREATE INDEX idx_profiles_country ON profiles(country); +``` + +--- + +## ๐Ÿงช Testing Query Patterns + +### Unit Testing Queries + +Test your query patterns: + +```go +func TestUserQueryBuilder(t *testing.T) { + // Setup test data + user1 := &User{Name: "John", Country: "US", Age: 25} + user2 := &User{Name: "Jane", Country: "CA", Age: 17} + + queries.CreateObject(user1) + queries.CreateObject(user2) + + // Test query builder + users, err := BuildUserQuery(map[string]interface{}{ + "country": "US", + "min_age": 18, + }).All() + + assert.NoError(t, err) + assert.Len(t, users, 1) + assert.Equal(t, "John", users[0].Value().Name) +} +``` + +### Integration Testing + +Test complex queries with real data: + +```go +func TestComplexUserQuery(t *testing.T) { + // Setup complex test scenario + setupTestData(t) + + // Test complex query + users, err := queries.GetQuerySet(&User{}). + Select("*", "Profile.*"). + Filter("IsActive", true). + Filter("TodoSet.Done", false). + Annotate("TodoCount", expr.Count("TodoSet.ID")). + Having("TodoCount__gte", 5). + OrderBy("-TodoCount"). + All() + + assert.NoError(t, err) + assert.True(t, len(users) > 0) + + // Verify annotations + for _, row := range users { + todoCount, exists := row.GetAnnotation("TodoCount") + assert.True(t, exists) + assert.GreaterOrEqual(t, todoCount.(int64), int64(5)) + } +} +``` + +--- + +## ๐Ÿ’ก Best Practices + +### Query Organization + +1. **Group Related Queries**: Keep related queries in the same module +2. **Use Descriptive Names**: Name your query functions clearly +3. **Document Complex Queries**: Add comments for complex query logic +4. **Test Edge Cases**: Test queries with empty results, large datasets, etc. + +### Performance Guidelines + +1. **Use Indexes**: Ensure proper database indexes for filtered/ordered fields +2. **Limit Results**: Always use `Limit()` for large datasets +3. **Select Specific Fields**: Don't select unnecessary fields +4. **Monitor Query Performance**: Use query analysis tools + +### Error Handling + +```go +func GetUserWithTodos(userID int) (*User, error) { + users, err := queries.GetQuerySet(&User{}). + Select("*", "TodoSet.*"). + Filter("ID", userID). + All() + + if err != nil { + return nil, fmt.Errorf("failed to fetch user %d: %w", userID, err) + } + + if len(users) == 0 { + return nil, fmt.Errorf("user %d not found", userID) + } + + return users[0].Value(), nil +} +``` + +--- + +Continue with [Expressions](../expressions/expressions.md) to learn about building complex query expressionsโ€ฆ \ No newline at end of file diff --git a/queries/docs/relations/relations.md b/queries/docs/relations/relations.md index e69de29b..2de34d34 100644 --- a/queries/docs/relations/relations.md +++ b/queries/docs/relations/relations.md @@ -0,0 +1,691 @@ +# Relations & Joins + +This document covers how to define and work with relationships between models in the `go-django-queries` package. + +The queries package supports relationships through field definitions and proxy models, enabling you to build complex queries across related data. + +--- + +## ๐Ÿ”— Types of Relations + +### Foreign Key (Many-to-One) + +A Foreign Key creates a many-to-one relationship where many instances of the current model can be related to one instance of another model. + +```go +type User struct { + models.Model + ID int64 + Name string + Email string +} + +type Todo struct { + models.Model + ID int64 + Title string + Description string + Done bool + User *User // Foreign Key to User +} + +func (m *Todo) FieldDefs() attrs.Definitions { + return m.Model.Define(m, + attrs.Unbound("ID", &attrs.FieldConfig{Primary: true}), + attrs.Unbound("Title"), + attrs.Unbound("Description"), + attrs.Unbound("Done"), + attrs.Unbound("User"), + ) +} +``` + +### One-to-One + +A One-to-One relationship links each instance of a model to exactly one instance of another model. This is commonly used for extending models with additional data. + +```go +type User struct { + models.Model + ID int64 + Name string + Email string +} + +type Profile struct { + models.Model + ID int64 + User *User // One-to-One relationship with User + Bio string + Location string +} + +func (m *Profile) FieldDefs() attrs.Definitions { + return m.Model.Define(m, + attrs.Unbound("ID", &attrs.FieldConfig{Primary: true}), + attrs.Unbound("User"), + attrs.Unbound("Bio"), + attrs.Unbound("Location"), + ) +} +``` + +### Proxy Models + +Proxy models allow you to create relationships between models through embedded structures. This is useful for creating complex hierarchical models: + +```go +type Page struct { + models.Model + ID int64 + Title string + Description string + PageID int64 + PageContentType *contenttypes.BaseContentType[attrs.Definer] +} + +type BlogPage struct { + models.Model + Proxy *Page `proxy:"true"` + PageID int64 + Author string + Tags []string + Category string + CategoryContentType *contenttypes.BaseContentType[attrs.Definer] +} + +func (b *BlogPage) FieldDefs() attrs.Definitions { + return b.Model.Define(b, + fields.Embed("Proxy"), + attrs.Unbound("PageID", &attrs.FieldConfig{Primary: true}), + attrs.Unbound("Author"), + attrs.Unbound("Tags"), + attrs.Unbound("Category"), + attrs.Unbound("CategoryContentType"), + ) +} +``` + +--- + +## ๐Ÿ” Querying Relations + +### Basic Filtering + +Filter by related field values: + +```go +// Get all todos for a specific user +todos, err := queries.GetQuerySet(&Todo{}). + Filter("User", userID). + All() + +// Get all todos for users with specific email +todos, err := queries.GetQuerySet(&Todo{}). + Filter("User__Email", "user@example.com"). + All() +``` + +### Using Expressions + +Use expressions for more complex relationship queries: + +```go +// Filter using expressions +todos, err := queries.GetQuerySet(&Todo{}). + Filter(expr.Q("User", userID)). + All() + +// Complex filtering with AND/OR +todos, err := queries.GetQuerySet(&Todo{}). + Filter(expr.Or( + expr.Q("User", userID), + expr.Q("Done", true), + )). + All() +``` + +### Joins and Selecting + +Select related data to avoid N+1 queries: + +```go +// Select related user data +todos, err := queries.GetQuerySet(&Todo{}). + Select("*", "User.*"). + Filter("Done", false). + All() + +// Access related data +for _, todo := range todos { + if todo.User != nil { + fmt.Printf("Todo: %s, User: %s\n", todo.Title, todo.User.Name) + } +} +``` + +--- + +## ๐Ÿ“Š Advanced Relationship Queries + +### Aggregating Related Data + +Count related objects: + +```go +// Count todos per user (using annotations) +users, err := queries.GetQuerySet(&User{}). + Annotate("TodoCount", expr.Count("ID")). + All() +``` + +### Complex Filtering + +Filter based on related object properties: + +```go +// Get users who have completed todos +users, err := queries.GetQuerySet(&User{}). + Filter(expr.Q("Todo__Done", true)). + Distinct(). + All() +``` + +### Subqueries + +Use subqueries for complex relationship filtering: + +```go +// Get todos from active users +activeTodos, err := queries.GetQuerySet(&Todo{}). + Filter("User", queries.Subquery( + queries.GetQuerySet(&User{}). + Filter("IsActive", true). + Values("ID"), + )). + All() +``` + +--- + +## ๐ŸŽฏ Best Practices + +### Performance Optimization + +1. **Use Select Related**: Always use `Select()` to load related data in one query +2. **Avoid N+1 Queries**: Load related data upfront rather than in loops +3. **Use Indexes**: Create database indexes on foreign key fields + +### Code Organization + +1. **Clear Relationships**: Make relationships explicit in model definitions +2. **Consistent Naming**: Use consistent naming conventions for related fields +3. **Documentation**: Document complex relationships in code comments + +### Testing + +```go +func TestUserTodos(t *testing.T) { + // Create test user + user := &User{Name: "Test User", Email: "test@example.com"} + _, err := queries.GetQuerySet(user).Create(user) + require.NoError(t, err) + + // Create test todos + todos := []*Todo{ + {Title: "Todo 1", Done: false, User: user}, + {Title: "Todo 2", Done: true, User: user}, + } + _, err = queries.GetQuerySet(&Todo{}).BulkCreate(todos) + require.NoError(t, err) + + // Test relationship query + userTodos, err := queries.GetQuerySet(&Todo{}). + Filter("User", user.ID). + All() + require.NoError(t, err) + assert.Len(t, userTodos, 2) +} +``` + +--- + +## ๐Ÿ”ง Advanced Features + +### Generic Relations + +Use content types for generic foreign keys: + +```go +type Comment struct { + models.Model + ID int64 + Content string + ContentType *contenttypes.BaseContentType[attrs.Definer] + ObjectID int64 +} + +func (c *Comment) FieldDefs() attrs.Definitions { + return c.Model.Define(c, + attrs.Unbound("ID", &attrs.FieldConfig{Primary: true}), + attrs.Unbound("Content"), + attrs.Unbound("ContentType"), + attrs.Unbound("ObjectID"), + ) +} +``` + +### Proxy Field Relations + +Create complex join conditions using proxy fields: + +```go +type BlogPageCategory struct { + models.Model + *BlogPage + Category string +} + +func (b *BlogPageCategory) FieldDefs() attrs.Definitions { + return b.Model.Define(b, + fields.Embed("BlogPage"), + attrs.Unbound("Category", &attrs.FieldConfig{Primary: true}), + ) +} +``` + +This comprehensive guide covers the relationship patterns used in the go-django-queries package. All examples are based on actual test patterns and should work with the current implementation. + +```go +type Profile struct { + models.Model + ID int + Name string + Email string + User *User // One-to-One relationship +} + +func (m *Profile) FieldDefs() attrs.Definitions { + return m.Model.Define(m, + attrs.NewField(m, "ID", &attrs.FieldConfig{ + Primary: true, + }), + attrs.NewField(m, "Name", nil), + attrs.NewField(m, "Email", nil), + attrs.NewField(m, "User", &attrs.FieldConfig{ + RelOneToOne: attrs.Relate(&User{}, "", nil), + Column: "user_id", + }), + ).WithTableName("profiles") +} +``` + +### One-to-Many (Reverse Foreign Key) + +When you define a Foreign Key, the related model automatically gets a reverse One-to-Many relationship. + +```go +// User model automatically has a reverse relationship to Todo +// This is accessible via the model's reverse relations +``` + +### Many-to-Many + +A Many-to-Many relationship allows multiple instances of one model to be related to multiple instances of another model. + +```go +type Author struct { + models.Model + ID int + Name string + Books []*Book // Many-to-Many relationship +} + +type Book struct { + models.Model + ID int + Title string + Authors []*Author // Many-to-Many relationship +} + +func (m *Author) FieldDefs() attrs.Definitions { + return m.Model.Define(m, + attrs.NewField(m, "ID", &attrs.FieldConfig{ + Primary: true, + }), + attrs.NewField(m, "Name", nil), + attrs.NewField(m, "Books", &attrs.FieldConfig{ + RelManyToMany: attrs.Relate(&Book{}, "authors", nil), + }), + ).WithTableName("authors") +} + +func (m *Book) FieldDefs() attrs.Definitions { + return m.Model.Define(m, + attrs.NewField(m, "ID", &attrs.FieldConfig{ + Primary: true, + }), + attrs.NewField(m, "Title", nil), + attrs.NewField(m, "Authors", &attrs.FieldConfig{ + RelManyToMany: attrs.Relate(&Author{}, "books", nil), + }), + ).WithTableName("books") +} +``` + +### Many-to-Many with Through Model + +Sometimes you need to store additional information about the relationship itself. This is done using a "through" model. + +```go +type Author struct { + models.Model + ID int + Name string + Books []*Book // Many-to-Many through AuthorBook +} + +type Book struct { + models.Model + ID int + Title string + Authors []*Author // Many-to-Many through AuthorBook +} + +type AuthorBook struct { + models.Model + ID int + Author *Author + Book *Book + Role string // Additional field: author's role in the book + OrderBy int // Additional field: order of authorship +} + +func (m *Author) FieldDefs() attrs.Definitions { + return m.Model.Define(m, + attrs.NewField(m, "ID", &attrs.FieldConfig{ + Primary: true, + }), + attrs.NewField(m, "Name", nil), + attrs.NewField(m, "Books", &attrs.FieldConfig{ + RelManyToMany: attrs.Relate(&Book{}, "authors", &AuthorBook{}), + }), + ).WithTableName("authors") +} + +func (m *AuthorBook) FieldDefs() attrs.Definitions { + return m.Model.Define(m, + attrs.NewField(m, "ID", &attrs.FieldConfig{ + Primary: true, + }), + attrs.NewField(m, "Author", &attrs.FieldConfig{ + RelForeignKey: attrs.Relate(&Author{}, "", nil), + Column: "author_id", + }), + attrs.NewField(m, "Book", &attrs.FieldConfig{ + RelForeignKey: attrs.Relate(&Book{}, "", nil), + Column: "book_id", + }), + attrs.NewField(m, "Role", nil), + attrs.NewField(m, "OrderBy", nil), + ).WithTableName("author_books") +} +``` + +--- + +## ๐Ÿ“ Querying Relations + +### Forward Relations + +Forward relations are accessed by referencing the field name directly: + +```go +// Get all todos with their related user +todos, err := queries.GetQuerySet(&Todo{}). + Select("*", "User.*"). + All() + +// Filter by related field +todos, err := queries.GetQuerySet(&Todo{}). + Filter("User.Name", "John"). + All() + +// Order by related field +todos, err := queries.GetQuerySet(&Todo{}). + OrderBy("User.Name"). + All() +``` + +### Reverse Relations + +Reverse relations are accessed using the model name followed by "Set": + +```go +// Get all users with their related todos +users, err := queries.GetQuerySet(&User{}). + Select("*", "TodoSet.*"). + All() + +// Filter by reverse relation +users, err := queries.GetQuerySet(&User{}). + Filter("TodoSet.Done", false). + All() + +// Count related objects +users, err := queries.GetQuerySet(&User{}). + Annotate("TodoCount", expr.Count("TodoSet.ID")). + All() +``` + +### Many-to-Many Relations + +```go +// Get all authors with their books +authors, err := queries.GetQuerySet(&Author{}). + Select("*", "Books.*"). + All() + +// Filter by many-to-many relation +authors, err := queries.GetQuerySet(&Author{}). + Filter("Books.Title__contains", "Harry Potter"). + All() + +// Get books with their authors +books, err := queries.GetQuerySet(&Book{}). + Select("*", "Authors.*"). + All() +``` + +### Through Model Relations + +When using through models, you can access both the related object and the through model: + +```go +// Get authors with books and the through model information +authors, err := queries.GetQuerySet(&Author{}). + Select("*", "Books.*", "BooksThrough.*"). + All() + +// Filter by through model fields +authors, err := queries.GetQuerySet(&Author{}). + Filter("BooksThrough.Role", "Primary Author"). + All() + +// Order by through model fields +authors, err := queries.GetQuerySet(&Author{}). + OrderBy("BooksThrough.OrderBy"). + All() +``` + +--- + +## ๐Ÿ” Advanced Querying + +### Nested Relations + +You can access nested relations using dot notation: + +```go +// Get todos with user and user's profile +todos, err := queries.GetQuerySet(&Todo{}). + Select("*", "User.*", "User.Profile.*"). + All() + +// Filter by nested relation +todos, err := queries.GetQuerySet(&Todo{}). + Filter("User.Profile.Email__contains", "@example.com"). + All() +``` + +### Joins + +The queries package automatically generates appropriate JOIN clauses based on the selected fields: + +```go +// This generates an INNER JOIN to User table +todos, err := queries.GetQuerySet(&Todo{}). + Select("*", "User.*"). + All() + +// This generates a LEFT JOIN to User table +todos, err := queries.GetQuerySet(&Todo{}). + Select("*", "User.*"). + Filter("User.ID__isnull", true). + All() +``` + +### Prefetch Relations + +For better performance when accessing multiple related objects: + +```go +// Prefetch all related users in a single query +todos, err := queries.GetQuerySet(&Todo{}). + Select("*", "User.*"). + All() + +// Access the related user without additional queries +for _, row := range todos { + todo := row.Value() + user := todo.User // Already loaded, no additional query +} +``` + +### Aggregations on Relations + +```go +// Count related objects +users, err := queries.GetQuerySet(&User{}). + Annotate("TodoCount", expr.Count("TodoSet.ID")). + All() + +// Sum related fields +users, err := queries.GetQuerySet(&User{}). + Annotate("CompletedTodos", expr.Count("TodoSet.ID")). + Filter("TodoSet.Done", true). + All() + +// Average of related fields +categories, err := queries.GetQuerySet(&Category{}). + Annotate("AvgTodos", expr.Avg("TodoSet.ID")). + All() +``` + +--- + +## ๐Ÿ’ก Best Practices + +### Performance Considerations + +1. **Select Only What You Need**: Use `Select()` to specify which fields and relations to load +2. **Avoid N+1 Queries**: Use relations in `Select()` to prefetch related data +3. **Use Aggregations**: Instead of loading all related objects, use aggregations when you only need counts or sums + +```go +// Good: Load user data with todos in one query +users, err := queries.GetQuerySet(&User{}). + Select("*", "TodoSet.*"). + All() + +// Bad: This will cause N+1 queries +users, err := queries.GetQuerySet(&User{}).All() +for _, row := range users { + user := row.Value() + // This causes an additional query for each user + todos, _ := queries.GetQuerySet(&Todo{}).Filter("User", user.ID).All() +} +``` + +### Naming Conventions + +1. **Field Names**: Use the related model name for the field (e.g., `User *User`) +2. **Reverse Relations**: Accessed as `ModelNameSet` (e.g., `TodoSet`) +3. **Through Models**: Accessed as `RelationNameThrough` (e.g., `BooksThrough`) + +### Error Handling + +```go +// Always check for errors when querying relations +todos, err := queries.GetQuerySet(&Todo{}). + Select("*", "User.*"). + All() +if err != nil { + // Handle error appropriately + return fmt.Errorf("failed to fetch todos with users: %w", err) +} +``` + +--- + +## ๐Ÿงช Testing Relations + +When testing models with relations, make sure to: + +1. Create test data for all related models +2. Test both forward and reverse relations +3. Test filtering and ordering by related fields +4. Test aggregations on related fields + +```go +func TestTodoUserRelation(t *testing.T) { + // Create test user + user := &User{Name: "Test User", Email: "test@example.com"} + err := queries.CreateObject(user) + assert.NoError(t, err) + + // Create test todo + todo := &Todo{ + Title: "Test Todo", + User: user, + } + err = queries.CreateObject(todo) + assert.NoError(t, err) + + // Test forward relation + todos, err := queries.GetQuerySet(&Todo{}). + Select("*", "User.*"). + Filter("ID", todo.ID). + All() + assert.NoError(t, err) + assert.Len(t, todos, 1) + assert.Equal(t, user.Name, todos[0].Value().User.Name) + + // Test reverse relation + users, err := queries.GetQuerySet(&User{}). + Select("*", "TodoSet.*"). + Filter("ID", user.ID). + All() + assert.NoError(t, err) + assert.Len(t, users, 1) + // Access todos through reverse relation + userData := users[0].Value().DataStore() + todoSet, exists := userData.GetValue("TodoSet") + assert.True(t, exists) + // todoSet should contain the related todos +} +``` + +--- + +Continue with [QuerySet Reference](../queryset/queryset.md) for more advanced querying techniquesโ€ฆ \ No newline at end of file diff --git a/queries/docs/virtual_fields.md b/queries/docs/virtual_fields.md index e69de29b..0fbba0d8 100644 --- a/queries/docs/virtual_fields.md +++ b/queries/docs/virtual_fields.md @@ -0,0 +1,414 @@ +# Virtual Fields + +This document covers virtual fields in the `go-django-queries` package, which allow you to create computed fields that are calculated at query time using SQL expressions. + +Virtual fields enable you to add dynamic, calculated values to your models without modifying your database schema. + +--- + +## ๐Ÿ”ฎ What are Virtual Fields? + +Virtual fields are computed fields that exist only during query execution. They are not stored in the database but are calculated on-the-fly using SQL expressions. + +Virtual fields are useful for: +- Field calculations (e.g., mathematical operations) +- String manipulations (e.g., concatenation, formatting) +- Conditional logic (e.g., status based on multiple conditions) +- Data aggregations (e.g., counts, sums) + +--- + +## ๐Ÿ—๏ธ Creating Virtual Fields + +### Basic Virtual Field with Annotations + +The most common way to create virtual fields is using annotations: + +```go +// Add a computed field to calculate user age +users, err := queries.GetQuerySet(&User{}). + Annotate("Age", expr.F("YEAR(CURRENT_DATE) - YEAR(![BirthDate])", "BirthDate")). + All() + +// Access the virtual field +for _, user := range users { + fmt.Printf("User: %s, Age: %d\n", user.Name, user.Age) +} +``` + +### String Concatenation + +Create virtual fields that combine multiple fields: + +```go +// Concatenate first and last name +users, err := queries.GetQuerySet(&User{}). + Annotate("FullName", expr.CONCAT( + expr.Field("FirstName"), + expr.Value(" "), + expr.Field("LastName"), + )). + All() +``` + +### Mathematical Operations + +Use logical expressions for calculations: + +```go +// Calculate total price with tax +products, err := queries.GetQuerySet(&Product{}). + Annotate("TotalPrice", + expr.Logical("Price").MUL(expr.Value(1.08)), + ). + All() + +// Calculate discount percentage +products, err := queries.GetQuerySet(&Product{}). + Annotate("DiscountPercent", + expr.Logical("OriginalPrice").SUB("CurrentPrice"). + DIV("OriginalPrice").MUL(expr.Value(100)), + ). + All() +``` + +--- + +## ๐Ÿ” Conditional Virtual Fields + +### Case-When Logic + +Create virtual fields based on conditional logic: + +```go +// Create status field based on multiple conditions +users, err := queries.GetQuerySet(&User{}). + Annotate("Status", + expr.Case( + expr.When(expr.Q("IsActive", true), "Active"), + expr.When(expr.Q("IsBlocked", true), "Blocked"), + expr.Value("Inactive"), + ), + ). + All() +``` + +### Complex Conditional Logic + +Use nested conditions for more complex logic: + +```go +// User classification based on activity +users, err := queries.GetQuerySet(&User{}). + Annotate("UserType", + expr.Case( + expr.When(expr.Q("IsAdmin", true), "Administrator"), + expr.When(expr.And( + expr.Q("PostCount__gte", 10), + expr.Q("IsActive", true), + ), "Power User"), + expr.When(expr.Q("PostCount__gte", 5), "Regular User"), + expr.Value("New User"), + ), + ). + All() +``` + +--- + +## ๐Ÿ“Š Aggregation Virtual Fields + +### Count Aggregations + +Use count functions for aggregating related data: + +```go +// Count related objects +users, err := queries.GetQuerySet(&User{}). + Annotate("PostCount", expr.Count("ID")). + All() + +// Conditional counting using case expressions +users, err := queries.GetQuerySet(&User{}). + Annotate("PublishedPostCount", + expr.Count( + expr.Case( + expr.When(expr.Q("Posts__Published", true), expr.Field("Posts__ID")), + expr.Value(nil), + ), + ), + ). + All() +``` + +### Other Aggregations + +Use various aggregation functions: + +```go +// Sum, average, min, max +orders, err := queries.GetQuerySet(&Order{}). + Annotate("TotalAmount", expr.Sum("OrderItems__Price")). + Annotate("AverageItemPrice", expr.Avg("OrderItems__Price")). + Annotate("MinItemPrice", expr.Min("OrderItems__Price")). + Annotate("MaxItemPrice", expr.Max("OrderItems__Price")). + All() +``` + +--- + +## ๐Ÿ—“๏ธ Date and Time Virtual Fields + +### Date Functions + +Create virtual fields for date operations: + +```go +// Extract parts of dates +posts, err := queries.GetQuerySet(&Post{}). + Annotate("Year", expr.F("YEAR(![CreatedAt])", "CreatedAt")). + Annotate("Month", expr.F("MONTH(![CreatedAt])", "CreatedAt")). + Annotate("DayOfWeek", expr.F("DAYOFWEEK(![CreatedAt])", "CreatedAt")). + All() +``` + +### Date Calculations + +Calculate time differences: + +```go +// Days since creation +posts, err := queries.GetQuerySet(&Post{}). + Annotate("DaysOld", + expr.F("DATEDIFF(CURRENT_DATE, ![CreatedAt])", "CreatedAt"), + ). + All() +``` + +--- + +## ๐Ÿ”ง Advanced Virtual Field Techniques + +### Raw SQL Expressions + +Use raw SQL for complex calculations: + +```go +// Complex string manipulation +users, err := queries.GetQuerySet(&User{}). + Annotate("InitializedName", + expr.Raw("CONCAT(LEFT(first_name, 1), '. ', last_name)"), + ). + All() +``` + +### Logical Expressions + +Use logical expressions for field operations: + +```go +// Update with calculated values +updated, err := queries.GetQuerySet(&Product{}). + Select("Price", "DiscountRate"). + Update(&Product{}, + expr.As("FinalPrice", + expr.Logical("Price").MUL( + expr.Logical(expr.Value(1)).SUB("DiscountRate"), + ), + ), + ) +``` + +--- + +## ๐ŸŽฏ Performance Considerations + +### Indexing Virtual Fields + +While virtual fields aren't stored, consider indexing the underlying fields: + +```sql +-- Index fields used in virtual field calculations +CREATE INDEX idx_users_birth_date ON users(birth_date); +CREATE INDEX idx_posts_created_at ON posts(created_at); +``` + +### Query Optimization + +Optimize virtual field queries: + +```go +// Use virtual fields in filtering efficiently +users, err := queries.GetQuerySet(&User{}). + Annotate("Age", expr.F("YEAR(CURRENT_DATE) - YEAR(![BirthDate])", "BirthDate")). + Filter("Age__gte", 18). // Filter on virtual field + All() +``` + +### Caching Strategies + +For expensive virtual field calculations, consider caching: + +```go +// Cache results of expensive calculations +type UserWithStats struct { + User + PostCount int `json:"post_count"` + AvgRating float64 `json:"avg_rating"` + LastPostDate time.Time `json:"last_post_date"` +} + +// Use computed fields for frequently accessed data +func GetUserStats(userID int64) (*UserWithStats, error) { + // Check cache first + if cached := getFromCache(userID); cached != nil { + return cached, nil + } + + // Compute and cache + user, err := queries.GetQuerySet(&User{}). + Annotate("PostCount", expr.Count("Posts")). + Annotate("AvgRating", expr.Avg("Posts__Rating")). + Annotate("LastPostDate", expr.Max("Posts__CreatedAt")). + Filter("ID", userID). + First() + + if err != nil { + return nil, err + } + + result := &UserWithStats{ + User: *user, + PostCount: user.PostCount, + AvgRating: user.AvgRating, + LastPostDate: user.LastPostDate, + } + + setCache(userID, result) + return result, nil +} +``` + +--- + +## ๐Ÿงช Testing Virtual Fields + +### Unit Tests + +Test virtual field calculations: + +```go +func TestVirtualFields(t *testing.T) { + // Setup test data + user := &User{ + FirstName: "John", + LastName: "Doe", + BirthDate: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC), + } + _, err := queries.GetQuerySet(user).Create(user) + require.NoError(t, err) + + // Test virtual field calculation + result, err := queries.GetQuerySet(&User{}). + Annotate("FullName", expr.CONCAT( + expr.Field("FirstName"), + expr.Value(" "), + expr.Field("LastName"), + )). + Filter("ID", user.ID). + First() + + require.NoError(t, err) + assert.Equal(t, "John Doe", result.FullName) +} +``` + +### Integration Tests + +Test virtual fields in complex scenarios: + +```go +func TestComplexVirtualFields(t *testing.T) { + // Test conditional virtual fields + users, err := queries.GetQuerySet(&User{}). + Annotate("Status", + expr.Case( + expr.When(expr.Q("IsActive", true), "Active"), + expr.Value("Inactive"), + ), + ). + Filter("Status", "Active"). + All() + + require.NoError(t, err) + for _, user := range users { + assert.Equal(t, "Active", user.Status) + assert.True(t, user.IsActive) + } +} +``` + +--- + +## ๐Ÿ“š Real-World Examples + +### E-commerce + +```go +// Product pricing with discounts +products, err := queries.GetQuerySet(&Product{}). + Annotate("FinalPrice", + expr.Case( + expr.When(expr.Q("OnSale", true), + expr.Logical("Price").MUL( + expr.Logical(expr.Value(1)).SUB("DiscountRate"), + ), + ), + expr.Field("Price"), + ), + ). + Annotate("Savings", + expr.Case( + expr.When(expr.Q("OnSale", true), + expr.Logical("Price").SUB("FinalPrice"), + ), + expr.Value(0), + ), + ). + All() +``` + +### User Analytics + +```go +// User engagement metrics +users, err := queries.GetQuerySet(&User{}). + Annotate("EngagementScore", + expr.Logical("PostCount").MUL(expr.Value(2)).ADD( + expr.Logical("CommentCount").MUL(expr.Value(1)), + ).ADD( + expr.Logical("LikeCount").MUL(expr.Value(0.5)), + ), + ). + OrderBy("-EngagementScore"). + All() +``` + +### Content Management + +```go +// Article statistics +articles, err := queries.GetQuerySet(&Article{}). + Annotate("WordCount", expr.LENGTH("Content")). + Annotate("ReadingTime", + expr.Logical(expr.LENGTH("Content")).DIV(expr.Value(200)), // Assuming 200 WPM + ). + Annotate("Popularity", + expr.Logical("ViewCount").MUL(expr.Value(0.3)).ADD( + expr.Logical("ShareCount").MUL(expr.Value(0.7)), + ), + ). + All() +``` + +This comprehensive guide covers virtual fields in the go-django-queries package, providing practical examples based on actual usage patterns. \ No newline at end of file