Skip to content

feat: laravel collection#1134

Merged
hwbrzzl merged 7 commits intogoravel:masterfrom
ahmed3mar:feature/laravel-collection
Mar 4, 2026
Merged

feat: laravel collection#1134
hwbrzzl merged 7 commits intogoravel:masterfrom
ahmed3mar:feature/laravel-collection

Conversation

@ahmed3mar
Copy link
Contributor

@ahmed3mar ahmed3mar commented Jul 19, 2025

📑 Description

Closes goravel/goravel#566

Basic Usage

Creating Collections

// Create from variadic arguments
numbers := collect.New(1, 2, 3, 4, 5)

// Create from slice
items := []string{"apple", "banana", "cherry"}
fruits := collect.Of(items)

Basic Operations

numbers := collect.New(1, 2, 3, 4, 5)

// Get count
fmt.Println(numbers.Count()) // 5

// Get first/last
fmt.Println(*numbers.First()) // 1
fmt.Println(*numbers.Last())  // 5

// Check if empty
fmt.Println(numbers.IsEmpty()) // false

// Get all items
fmt.Println(numbers.All()) // [1 2 3 4 5]

Filtering and Mapping

numbers := collect.New(1, 2, 3, 4, 5, 6)

// Filter even numbers
evens := numbers.Filter(func(n int, _ int) bool {
    return n%2 == 0
})
fmt.Println(evens.All()) // [2 4 6]

// Map to double values (Laravel-style method)
doubled := numbers.Map(func(n int, _ int) interface{} {
    return n * 2
})
fmt.Println(doubled.All()) // [2 4 6 8 10 12]

// Map to string representation
strings := numbers.Map(func(n int, i int) interface{} {
    return fmt.Sprintf("item_%d_%d", i, n)
})
fmt.Println(strings.All()) // [item_0_1 item_1_2 item_2_3 item_3_4 item_4_5 item_5_6]

### Working with Structs

```go
type User struct {
    ID   int
    Name string
    Age  int
}

users := collect.Of([]User{
    {ID: 1, Name: "Alice", Age: 25},
    {ID: 2, Name: "Bob", Age: 30},
    {ID: 3, Name: "Charlie", Age: 25},
})

// Filter by struct field using different Where patterns
youngUsers := users.Where("Age", "=", 25)        // 3 parameters
youngUsers = users.Where("Age", 25)              // 2 parameters (implies '=')
fmt.Println(youngUsers.Count()) // 2

// Using callback function
adultUsers := users.Where(func(u User) bool {
    return u.Age >= 18
})

// Using different operators
olderUsers := users.Where("Age", ">", 25)
notCharlie := users.Where("Name", "!=", "Charlie")

// Handling null values
activeUsers := users.Where("DeletedAt", "=", nil)    // Find non-deleted users
deletedUsers := users.Where("DeletedAt", "!=", nil)  // Find deleted users

// Group by field
grouped := users.GroupBy(func(u User) string {
    return fmt.Sprintf("%d", u.Age)
})
fmt.Println(len(grouped)) // 2 groups

// Pluck field values
names := users.Pluck("Name")
fmt.Println(names.Count()) // 3

// Sort by field
sorted := users.SortBy(func(u User) string {
    return u.Name
})
fmt.Println(sorted.First().Name) // "Alice"

Aggregation Methods

numbers := collect.New(1, 2, 3, 4, 5)

// Sum
sum := numbers.Sum(func(n int) float64 {
    return float64(n)
})
fmt.Println(sum) // 15.0

// Average
avg := numbers.Avg(func(n int) float64 {
    return float64(n)
})
fmt.Println(avg) // 3.0

// Min/Max
min := numbers.Min(func(n int) float64 { return float64(n) })
max := numbers.Max(func(n int) float64 { return float64(n) })
fmt.Println(min, max) // 1.0 5.0

Conditional Operations

numbers := collect.New(1, 2, 3, 4, 5)

// Conditional transformations
result := numbers.
    When(true, func(c *collect.Collection[int]) *collect.Collection[int] {
        return c.Filter(func(n int, _ int) bool { return n > 2 })
    }).
    Unless(false, func(c *collect.Collection[int]) *collect.Collection[int] {
        return c.Take(2)
    })

fmt.Println(result.All()) // [3 4]

// Tap for side effects
numbers.Tap(func(c *collect.Collection[int]) {
    fmt.Println("Processing", c.Count(), "items")
})

Available Methods

Core Methods (Alphabetical)

  • After(value) - Get item after given value
  • All() - Get all items as slice
  • Average(keyFunc) - Calculate average using key function
  • Before(value) - Get item before given value
  • Chunk(size) - Split into chunks of given size
  • Clone() - Create a copy of the collection
  • Collapse() - Collapse nested arrays into single array
  • Combine(keys) - Combine with keys to create map
  • Contains(value) - Check if collection contains value
  • Count() - Get item count
  • Diff(other) - Get difference with another collection
  • Each(func) - Iterate over each item
  • Every(predicate) - Check if all items match predicate
  • Filter(predicate) - Filter items by predicate
  • First() - Get first item
  • Flatten() - Flatten nested structures
  • GroupBy(keyFunc) - Group items by key function
  • Intersect(other) - Get intersection with another collection
  • IsEmpty() - Check if collection is empty
  • IsNotEmpty() - Check if collection is not empty
  • Join(separator) - Join items with separator
  • Last() - Get last item
  • Map(func) - Transform each item with a function (returns Collection[interface{}])
  • Merge(other) - Merge with another collection
  • Partition(predicate) - Split into two collections by predicate
  • Pluck(field) - Extract field values
  • Push(items...) - Add items to end
  • Reverse() - Reverse order
  • Search(value) - Find index of value
  • Slice(start, length) - Get slice of items
  • Sort(lessFunc) - Sort by comparison function
  • SortBy(keyFunc) - Sort by key function
  • Sum(keyFunc) - Calculate sum using key function
  • Take(n) - Take first n items
  • Unique() - Get unique items
  • Where(field, operator, value) - Filter by field comparison
  • Zip(other) - Zip with another collection

Where Method - Laravel-style Filtering

The Where method supports multiple patterns for flexible filtering:

type User struct {
    ID        int
    Name      string
    Age       int
    Country   string
    Balance   float64
    DeletedAt *time.Time
}

users := collect.Of([]User{...})

// 1. Two parameters (field, value) - implies '=' operator
frenchUsers := users.Where("Country", "FR")
youngUsers := users.Where("Age", 25)

// 2. Three parameters (field, operator, value)
richUsers := users.Where("Balance", ">", 100.0)
nonFrenchUsers := users.Where("Country", "!=", "FR")
seniorUsers := users.Where("Age", ">=", 65)

// 3. Single parameter (callback function)
customFilter := users.Where(func(u User) bool {
    return u.Age > 18 && u.Country == "US"
})

// 4. Null comparisons
activeUsers := users.Where("DeletedAt", "=", nil)
deletedUsers := users.Where("DeletedAt", "!=", nil)

// 5. String operations
nameContains := users.Where("Name", "like", "john")
excludePattern := users.Where("Name", "not like", "test")

Supported Operators:

  • =, == - Equality
  • != - Inequality
  • >, >= - Greater than, Greater than or equal
  • <, <= - Less than, Less than or equal
  • like - Case-insensitive substring match
  • not like - Case-insensitive substring exclusion

Utility Methods

  • Debug() - Print collection contents
  • Dump() - Print collection contents
  • Tap(func) - Execute function and return collection
  • ToJSON() - Convert to JSON string
  • When(condition, func) - Execute function if condition is true
  • Unless(condition, func) - Execute function if condition is false

Map Method - Laravel-style Transformation

The Map method provides Laravel-style transformation capabilities:

type User struct {
    ID   int
    Name string
    Age  int
}

users := collect.Of([]User{
    {ID: 1, Name: "Alice", Age: 25},
    {ID: 2, Name: "Bob", Age: 30},
})

// Transform to different types
names := users.Map(func(u User, i int) interface{} {
    return u.Name
})

ages := users.Map(func(u User, i int) interface{} {
    return u.Age
})

// Complex transformations
summaries := users.Map(func(u User, i int) interface{} {
    return map[string]interface{}{
        "id":      u.ID,
        "summary": fmt.Sprintf("%s (%d years)", u.Name, u.Age),
        "index":   i,
    }
})

// Chain with other operations
result := users.
    Map(func(u User, i int) interface{} {
        return u.Name
    }).
    Filter(func(name interface{}, _ int) bool {
        return len(name.(string)) > 3
    })

Generic Functions

Some operations require type transformation and are provided as generic functions for type safety:

// Type-safe Map to different type
strings := collect.New("1", "2", "3")
numbers := collect.Map(strings, func(s string, _ int) int {
    n, _ := strconv.Atoi(s)
    return n
})

// Reduce to single value
sum := collect.Reduce(numbers, func(acc int, n int, _ int) int {
    return acc + n
}, 0)

// Pluck with type conversion
users := collect.Of([]User{...})
userIDs := collect.Pluck[User, int](users, "ID")

Testing

go test -v

Examples

See example_test.go for comprehensive usage examples.

LazyCollection

LazyCollection provides lazy evaluation for efficient processing of large datasets. Operations are not executed until a terminal operation is called.

Creating LazyCollections

// From slice
lazy := collect.LazyCollect([]int{1, 2, 3, 4, 5})

// From range
lazy := collect.LazyRange(1, 1000000)

// From generator function
lazy := collect.LazyGenerate(func(i int) int {
    return i * 2
}, 100)

// From channel
ch := make(chan int, 5)
// ... populate channel
lazy := collect.LazyFromChannel(ch)

Lazy Operations

// Chain operations - these don't execute until consumed
result := collect.LazyRange(1, 1000000).
    Filter(func(n int, _ int) bool { return n%2 == 0 }).
    Take(5).
    All() // This triggers execution

fmt.Println(result) // [2 4 6 8 10]

Performance Benefits

// Efficient for large datasets when only a portion is needed
result := collect.LazyRange(1, 1000000).
    Filter(func(n int, _ int) bool { return n%100 == 0 }).
    Take(10).
    All()

// Only processes what's needed, not all 1 million items

Lazy vs Eager

// Lazy - processes only what's needed
lazyResult := collect.LazyRange(1, 1000000).
    Filter(func(n int, _ int) bool { return n%2 == 0 }).
    Take(5).
    All()

// Eager - processes all items first
eagerResult := collect.New(/* large slice */).
    Filter(func(n int, _ int) bool { return n%2 == 0 }).
    Take(5).
    All()

Converting Between Collections

// Lazy to eager
lazy := collectioncollectLazyRange(1, 11)
eager := lazy.Collect()

// Eager to lazy
eager := collect.New(1, 2, 3, 4, 5)
lazy := collect.LazyCollect(eager.All())

LazyCollection Methods

  • All() - Materialize all items
  • Count() - Count items
  • Filter(predicate) - Filter items
  • Map(func) - Transform each item with a function (returns LazyCollection[interface{}])
  • Where(params...) - Laravel-style filtering (supports all same patterns as Collection)
  • Take(n) - Take first n items
  • Skip(n) - Skip first n items
  • TakeWhile(predicate) - Take while condition is true
  • DropWhile(predicate) - Drop while condition is true
  • Unique() - Get unique items
  • Sort(lessFunc) - Sort items
  • Reverse() - Reverse order
  • Sum(keyFunc) - Calculate sum
  • Average(keyFunc) - Calculate average
  • Min(keyFunc) - Find minimum
  • Max(keyFunc) - Find maximum
  • GroupBy(keyFunc) - Group items
  • Partition(predicate) - Split into two collections
  • FlatMap(func) - Flat map transformation
  • Each(func) - Execute function for each item
  • ForEach(func) - Execute function for each item (consumes collection)
  • Iterator() - Get iterator for manual control
  • Collect() - Convert to eager Collection

License

MIT License

✅ Checks

  • Added test cases for my code

@codecov
Copy link

codecov bot commented Jul 19, 2025

Codecov Report

❌ Patch coverage is 77.11027% with 301 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.22%. Comparing base (bff7225) to head (59c4c40).
⚠️ Report is 227 commits behind head on master.

Files with missing lines Patch % Lines
support/collect/collection.go 76.83% 155 Missing and 28 partials ⚠️
support/collect/lazy_collection.go 77.52% 113 Missing and 5 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1134      +/-   ##
==========================================
+ Coverage   66.81%   69.22%   +2.40%     
==========================================
  Files         214      266      +52     
  Lines       14050    16833    +2783     
==========================================
+ Hits         9387    11652    +2265     
- Misses       4287     4685     +398     
- Partials      376      496     +120     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

return &Collection[T]{items: flattened}
}

func Collect[T any](items []T) *Collection[T] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about changing collect.Collect to collect.Of? This way, it avoids redundancy and becomes more concise.

@ahmed3mar ahmed3mar marked this pull request as ready for review November 29, 2025 18:49
@ahmed3mar ahmed3mar requested a review from a team as a code owner November 29, 2025 18:49
Copilot AI review requested due to automatic review settings November 29, 2025 18:49
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a comprehensive Laravel-style collection library for Go, providing both eager (Collection) and lazy (LazyCollection) evaluation strategies with over 100 methods for data manipulation.

Key Changes:

  • Introduces generic Collection and LazyCollection types with extensive method sets
  • Implements Laravel-style Where filtering with multiple parameter patterns
  • Provides lazy evaluation support for efficient processing of large datasets

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
support/collect/collection.go Core eager collection implementation with 100+ methods for filtering, mapping, sorting, and aggregation operations
support/collect/collection_test.go Comprehensive test suite covering all collection methods with edge cases and error scenarios
support/collect/lazy_collection.go Lazy evaluation implementation using channels and goroutines for efficient streaming operations
support/collect/lazy_collection_test.go Test suite for lazy collection functionality including performance tests and lazy evaluation validation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}
result = append(result, []T{item1, item2})
}

Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential goroutine leak: The Zip method may leave the ch2 channel's goroutine running if ch1 completes first. After breaking from the loop at line 855, any remaining items in ch2 won't be consumed, potentially blocking the goroutine that's feeding it.

Consider draining ch2 after the loop:

// Drain remaining items from ch2 to prevent goroutine leak
for range ch2 {
}
Suggested change
// Drain remaining items from ch2 to prevent goroutine leak
for range ch2 {
}

Copilot uses AI. Check for mistakes.
Comment on lines +658 to +676
func (lc *LazyCollection[T]) Take(n int) *LazyCollection[T] {
newPipeline := make([]func(<-chan T) <-chan T, len(lc.pipeline))
copy(newPipeline, lc.pipeline)

newPipeline = append(newPipeline, func(input <-chan T) <-chan T {
output := make(chan T)
go func() {
defer close(output)
taken := 0
for item := range input {
if taken >= n {
break
}
output <- item
taken++
}
}()
return output
})
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential goroutine leak: When Take breaks early at line 669, the input channel may not be fully consumed, which could block upstream goroutines. This is a common issue in lazy evaluation pipelines.

Consider draining the input channel after breaking:

for item := range input {
    if taken >= n {
        // Drain remaining items to prevent blocking upstream
        for range input {
        }
        break
    }
    output <- item
    taken++
}

Copilot uses AI. Check for mistakes.
Comment on lines +684 to +700
func (lc *LazyCollection[T]) TakeWhile(predicate func(T) bool) *LazyCollection[T] {
newPipeline := make([]func(<-chan T) <-chan T, len(lc.pipeline))
copy(newPipeline, lc.pipeline)

newPipeline = append(newPipeline, func(input <-chan T) <-chan T {
output := make(chan T)
go func() {
defer close(output)
for item := range input {
if !predicate(item) {
break
}
output <- item
}
}()
return output
})
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential goroutine leak: Similar to Take, when TakeWhile breaks early at line 694, the input channel may not be fully consumed, potentially blocking upstream goroutines.

Consider draining the input channel after breaking to prevent goroutine leaks.

Copilot uses AI. Check for mistakes.
Comment on lines +613 to +621
func (lc *LazyCollection[T]) Some(predicate func(T) bool) bool {
ch := lc.execute()
for item := range ch {
if predicate(item) {
return true
}
}
return false
}
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential goroutine leak: When Some returns early at line 617, the remaining items in the channel won't be consumed, potentially blocking upstream goroutines.

Consider draining the channel after returning true:

if predicate(item) {
    // Drain to prevent blocking
    go func() { for range ch {} }()
    return true
}

Copilot uses AI. Check for mistakes.
Comment on lines +245 to +251
func (lc *LazyCollection[T]) First() *T {
ch := lc.execute()
for item := range ch {
return &item
}
return nil
}
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential goroutine leak: When First returns early at line 248, remaining items in the channel won't be consumed, potentially blocking upstream goroutines. Similar issue exists in FirstWhere (lines 261-268) and Every (lines 210-218).

Consider draining the channel after early returns.

Copilot uses AI. Check for mistakes.
Comment on lines +1220 to +1239
func (c *Collection[T]) Zip(other *Collection[T]) [][]T {
maxLen := len(c.items)
if len(other.items) > maxLen {
maxLen = len(other.items)
}

var result [][]T
for i := 0; i < maxLen; i++ {
var pair []T
if i < len(c.items) {
pair = append(pair, c.items[i])
}
if i < len(other.items) {
pair = append(pair, other.items[i])
}
result = append(result, pair)
}

return result
}
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Zip method in Collection includes partial pairs when collections have different lengths, while LazyCollection.Zip stops at the shorter collection. This inconsistency in behavior could confuse users.

Collection.Zip produces: [[1], [2, 3]] for [1, 2] and [3]
LazyCollection.Zip produces: [] for the same inputs

Consider making the behavior consistent between both implementations. The Laravel-style typically stops at the shorter collection (like LazyCollection does).

Copilot uses AI. Check for mistakes.
return &Collection[interface{}]{items: mapped}
}

// MapCollect it will be renamed to Map in next release
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammar issue: "it will be renamed" should be "It will be renamed" (capitalize first letter of sentence).

Suggested change
// MapCollect it will be renamed to Map in next release
// MapCollect It will be renamed to Map in next release

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing PR 👍 @ahmed3mar Could you confirm the Copilot comments? Basically, LGTM.

Comment on lines +42 to +44
func (it *lazyIterator[T]) Reset() {
it.done = false
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the function can be used as expect when it.ch is closed, could you confirm this and add a test case for it?

taken := 0
for item := range input {
if taken >= n {
break
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There may still be data in the input chan, but break directly. goroutine will leak. The same as TakeWhile

defer close(output)
seen := make(map[string]bool)
for item := range input {
key := fmt.Sprintf("%v", item)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key will duplicated when item is struct or map, is there a better way to deal with such situation?

Comment on lines +849 to +853
ch1 := lc.execute()
ch2 := other.execute()

for item1 := range ch1 {
item2, ok2 := <-ch2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same as above, ch1 and ch2 may be leak.

Suggested change
ch1 := lc.execute()
ch2 := other.execute()
for item1 := range ch1 {
item2, ok2 := <-ch2
ch1 := lc.execute()
ch2 := other.execute()
defer func() {
go func() { for range ch1 {} }()
go func() { for range ch2 {} }()
}()
for item1 := range ch1 {
item2, ok2 := <-ch2

seen[key] = true
}
}
return &Collection[T]{items: duplicates}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the collecction is 3,3,3, duplicates will be 3,3. It's unexpected.

var duplicates []T

for _, item := range c.items {
key := fmt.Sprintf("%v", item)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same as LazyCollection, %v is unused for struct and map.

Copy link
Member

@krishankumar01 krishankumar01 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation and the wide range of methods are solid. Instead of implementing everything in a single PR, should we break it into smaller PRs so that reviewing becomes easier?
cc: @hwbrzzl

return 0
}

max := keyFunc(c.items[0])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about renaming this variable to something else, since it collides with Go’s built-in max method (and similarly for min)?

if len(c.items) == 0 {
return nil
}
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initializing a random generator every time is slower. How about we initialize a global generator and reuse it? And why this method is returning a pointer of type T?


func Reduce[T, R any](c *Collection[T], fn func(R, T, int) R, initial R) R {
result := initial
for i, item := range c.items {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we keep the input order as the index and use the remaining arguments for the fn(same for all the places where we pass index in function)?

return -1
}

func (c *Collection[T]) Shift() *T {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why return a pointer to a generic type? If the user needs a pointer, they can simply assign T as a pointer type anyway, right? Can we fix similar issue in other methods also?

shuffled := make([]T, len(c.items))
copy(shuffled, c.items)

rng := rand.New(rand.NewSource(time.Now().UnixNano()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

start = len(c.items)
}

end := start + deleteCount
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the user accidentally passes a negative deleteCount? It would end up duplicating the elements, right?

for i, item := range c.items {
c.items[i] = fn(item, i)
}
return c
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we return the original collection in some places and create a new one in others? How about always returning a new collection so the user has fewer surprises? For example, Transform also modifies the original if that's the expected behavior, how about documenting it?

func (c *Collection[T]) Union(other *Collection[T]) *Collection[T] {
existing := make(map[string]bool)
for _, item := range c.items {
existing[fmt.Sprintf("%v", item)] = true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very performance-heavy. For example, if T is a large User object, checking it this way will be significantly slower. In any case, we should not use the fmt package for key generation. For example:

type BigUser struct {
    ID       int
    Name     string
    Bio      string // Long text
    Metadata map[string]string
}

// IF YOU USE fmt.Sprintf("%v", user):
// 1. Go has to reflectively walk through ID, Name, Bio, and the Map.
// 2. It allocates a massive string for EVERY user.
// 3. It compares massive strings.

// IF YOU USE a proper Key (User.ID):
// 1. Go compares one integer. 
// 2. Zero allocation.
// 3. Instant.

So maybe we can accept a keyFunc as well, right? Also, this issue exists in every method that checks for seen and uses fmt.

}
}

func (c *Collection[T]) WhereIn(field string, values []interface{}) *Collection[T] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using this method will be a lot harder than expected because every time we use it we will need to convert the values to []any which a lot of pain, workaround would be to accept it like ...any.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's a bit complicated, but let's keep []any for now, given orm has the same function as well.

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we can optimize here since we already know the issue, right? For example, we accept ...any in WhereAny, Where, and all other Where methods except WhereIn and WhereNotIn.

Screenshot 2025-12-02 at 9 54 46 PM

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

args ...any is for = [value] or > [value], etc. values []any is actually for a slice. So basically, they are a bit different. And when I have a slice, eg: []string, there is no difference between []any and ...any.

image

[]string need to be transformed to []any as well. So I think []any is good for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm make sense


switch operator {
case "=", "==":
return reflect.DeepEqual(*fieldValue, value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an issue or inconsistent behaviour, for example:

func main() {
	p := Product{Price: 100}
	c := Of([]Product{p})

	// "100" (int) > "50" (string) -> True
	fmt.Println(c.Where("Price", ">", "50").Count()) // Output: 1 (Found)

	// "100" (int) == "100" (string) -> False
	fmt.Println(c.Where("Price", "=", "100").Count()) // Output: 0 (Not Found!)
}

@krishankumar01
Copy link
Member

I recommend against merging methods that are ambiguous, handle edge cases incorrectly, or introduce significant performance bottlenecks. It is better to omit these features than to expose unsafe or unoptimized implementations.

@hwbrzzl
Copy link
Contributor

hwbrzzl commented Dec 2, 2025

The implementation and the wide range of methods are solid. Instead of implementing everything in a single PR, should we break it into smaller PRs so that reviewing becomes easier? cc: @hwbrzzl

Yes, we recommend multiple small PRs instead of a big one. It's hard to review it. We can follow this next time.

Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval

Approving this PR to merge the comprehensive Collection library implementation. 🎉

Why We're Merging

  1. Core functionality is solid - Provides 100+ Laravel-style collection methods with both eager and lazy evaluation
  2. Highly requested feature - Closes goravel/goravel#566 which has been pending
  3. Strong foundation - Generic implementation with comprehensive method coverage
  4. Team consensus - @hwbrzzl and @krishankumar01 both acknowledge the solid implementation
  5. Issues are non-blocking - All identified issues can be addressed in follow-up PRs

Follow-up Issue Created

All review comments, concerns, and improvement suggestions have been documented in:
goravel/goravel#883

This includes:

  • Critical goroutine leak fixes
  • Performance optimizations (Random generator, key generation)
  • Behavioral consistency improvements
  • API design enhancements
  • Edge case handling
  • Documentation improvements
  • Test coverage expansion

Next Steps

The team can address issues in focused, reviewable PRs:

  1. Critical goroutine leaks (priority 1)
  2. Performance improvements (priority 2)
  3. API consistency fixes
  4. Enhanced test coverage

This approach allows the community to start using the Collection library while we iteratively improve it based on real-world feedback.

Thank you @ahmed3mar for this substantial contribution! 🙏

@hwbrzzl hwbrzzl merged commit 29a6104 into goravel:master Mar 4, 2026
20 checks passed
hwbrzzl added a commit that referenced this pull request Mar 11, 2026
* chore: Update upgrade DB packages (#1374)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Internalize file rotation logic from goravel/file-rotatelogs (#1375)

* Initial plan

* Implement internal file rotation to replace goravel/file-rotatelogs

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* Improve cleanup logic with glob pattern matching and add comprehensive tests

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* Address code review feedback: fix trailing whitespace, add cleanup wait, and make tests deterministic

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* optimize

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>
Co-authored-by: Bowen <hwbrzzl@gmail.com>

* chore: Update non-major dependencies (#1373)

* chore: Update non-major dependencies

* optimize

* optimize

* renovate/non-major-dependencies

* optimize

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Bowen <hwbrzzl@gmail.com>

* feat: [#726] Add HTTP server and client telemetry instrumentation [5] (#1326)

* add http middleware

* add http transport

* optimise http telemetry package

* add test cases for Telemetry middleware

* add auto instrumentation configs

* add config to enable Telemetry for http clients

* add docs for config facade

* add ConfigFacade nil warning

* disable default telemetry

* optimise transport

* add kill switch for instrumentation

* update the stubs

* remove unnecessary handler

* optimise log instrumentation

* move route registration in the end

* lazily initialize middleware and transport to work with new application_builder

* optimise channel test

* optimise channel test

* accept telemetry facade as an input instead of using global instance

* use a callback to resolve the telemetry facade instance

* optimise grpc handler to remove usage of telemetry and config facade

* optimise the grpc handler

* use telemetry transport if enabled

* optimise http auto instrumentation

* optimize log test cases

* optimise

* optimise

* optimise

* optimise

* revert PR#1357

* fix log test cases

* revert GRPC changes

* optimise middleware

* remove zipkin trace driver

* correct GRPC enable condition

* correct http transport enable condition

* correct log channel enable condition

* update go mod

* fix test cases

* fix test cases

* fix test cases

* fix tests

---------

Co-authored-by: Bowen <hwbrzzl@gmail.com>

* chore: optimize runner tests for Windows (#1377)

Co-authored-by: Bowen <hwbrzzl@github.com>

* chore: Update non-major dependencies (#1378)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix: runner stuck (#1381)

* fix: runner stuck

* optimize

* optimize

* optimize

* optimize

* optimize

* chore: Update non-major dependencies (#1382)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* feat: [#849] Add table support to the console context (#1380)

* add table function in cli

* add test cases for new method Table

* update mocks

* add column level styles

* fix lint error

* move style vars to new file

* add GlobalHuhTheme

* update go mod tidy

* feat: [#546] artisan command up and down to set website Maintenance mode (#1198)

* Add up and down command

* Use file helper to create files and add unit tests

* Fix the foundation.App dependency

* Use support/path instead of foundation.App

* Add unit test case

* Add some missing checks

* Add more missing checks

* One more

* Fix tmpfile path

* Use T.TempDir instead of os.TempDir in tests

* Add reason to the down command

* Add option to the tests

* Change the maintenance file name

* close created file handles

* Defer abort

* Fix the linter issue

* Add more options to the down command

* Use options

* Check for maintenance mode respond

* Fix down_command_test

* Add more unit test cases

* Fix more lint issues

* Fix lint issue

* fix tests

* fix tests

* fix tests

* fix tests

* Address PR comments

* Fix check_for_maintenance_test

* Check Aborts in check_for_maintenance

* Rename check_for_maintenance_mode

* Fix and Write more unit tests for check_for_maintenance_mode middleware

* fix down command

* Fix down_command_test

* Fix up_command

---------

Co-authored-by: Bowen <hwbrzzl@gmail.com>

* feat: Laravel-style Collection library (#1134)

Merges comprehensive Collection library with 100+ methods for data manipulation.

Implements both eager (Collection) and lazy (LazyCollection) evaluation strategies.
Closes goravel/goravel#566

Follow-up improvements tracked in: goravel/goravel#883

* Increase route module coverage for factory, provider, and runner wiring paths (#1384)

* Initial plan

* test(route): cover route factory and service provider paths

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* optimize

* optimize

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>
Co-authored-by: Bowen <hwbrzzl@gmail.com>

* chore: optimize interface (#1389)

* Increase crypt module coverage by exercising AES key validation and failure paths (#1385)

* Initial plan

* test: add crypt AES edge-case coverage

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test: improve readability of crypt key-length cases

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test: address crypt PR feedback and simplify test structure

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test: use configmock EXPECT style in TestNewAES

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test: standardize config mock setup across aes tests

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* Increase filesystem module test coverage for storage, service provider, and file facade paths (#1387)

* Initial plan

* Add filesystem application and provider coverage tests

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* Polish filesystem coverage tests after review feedback

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* Refine filesystem tests based on review feedback

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* Apply review style suggestions in filesystem tests

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* Increase mail module test coverage to 70%+ with focused unit tests (#1386)

* Initial plan

* test(mail): add focused unit coverage for application, options, and service provider

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test(mail): address review feedback on test specificity and readability

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test(mail): merge application unit tests and remove untyped mock matchers

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test(mail): deduplicate matchers and tighten queue job assertion

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test(mail): use AnythingOfType and any in application tests

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test(mail): derive bind callback type string from any signature

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test(mail): simplify bind callback type matcher

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test(mail): use AnythingOfType for template render expectations

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test(mail): restore specific MatchedBy assertions per review clarification

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* Increase hash module coverage with targeted tests for driver selection and service provider paths (#1388)

* Initial plan

* test: increase hash module coverage for driver and provider paths

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test: cover hash provider and driver selection paths

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test: address hash module review feedback

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test: merge hash module tests into application_test

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test: use EXPECT API in hash service provider test

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test: use any alias in hash singleton callback test

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* chore: add AI instructions file (#1393)

* Increase core framework coverage via targeted service provider and builder-path tests (#1391)

* Initial plan

* Add service provider coverage tests for core modules

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* Expand core module coverage tests and validate suite

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* Revert unintended test module dependency drift

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* Address actionable PR review suggestions in coverage tests

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* Finalize review feedback handling

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* Revert unintended tests module dependency updates

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* Tighten provider test matchers per review feedback

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* Increase infrastructure module coverage by adding Process/Packages/Log/Schedule tests and fixing errors.As target forwarding (#1392)

* Initial plan

* test(errors): cover As/Unwrap/Ignore/Join edge cases

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test(errors): use wrapped error in As coverage test

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* chore: revert unintended tests module file changes

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test: add coverage for process packages log schedule modules

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test: refine schedule coverage tests per review

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* chore: revert unintended tests module dependency updates

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test: assert boot no-panic in log and process providers

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* chore: finalize review feedback responses for boot test assertions

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* chore: revert unintended tests module file changes

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test: expand service-layer coverage across Event, Queue, gRPC, Cache, and Console (#1390)

* Initial plan

* test(event): cover service provider and queued listener dispatch paths

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* chore: finalize event coverage validation

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* chore: revert unintended tests module dependency updates

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test(event): address review suggestions for robust command matching and queue error path

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* chore: finalize PR feedback follow-up

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* chore: revert unintended tests module file updates

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test: add focused coverage cases for queue grpc cache console

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test: clarify queue nil args in empty chain test

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* chore: revert unintended tests module dependency updates

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* test: avoid nil context in console usage-error test

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* chore: finalize lint ci fix validation

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* chore: revert unintended tests module dependency drift

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* chore: Update non-major dependencies (#1399)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore: add generate flag to artisan build command (#1402)

* feat: [#911] add query context accessor (#1401)

* chore: optimize agents (#1396)

* chore: optimize agents

* optimize

* address comments

* Initial plan

* fix: backport migrate rollback default handling to v1.17.x

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* chore: align branch tree with v1.17.x before backporting rollback fix

Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>

* optimize

* optimize

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Bowen <hwbrzzl@gmail.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hwbrzzl <24771476+hwbrzzl@users.noreply.github.com>
Co-authored-by: krishan kumar <84431594+krishankumar01@users.noreply.github.com>
Co-authored-by: Bowen <hwbrzzl@github.com>
Co-authored-by: Mohan Raj <praem1990@gmail.com>
Co-authored-by: Ahmed M. Ammar <ahmed3mar@outlook.com>
Co-authored-by: 耗子 <haozi@loli.email>
Co-authored-by: ALMAS <almas.cc@icloud.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ [Feature] Add Full-Featured Collections Support

5 participants