Skip to content

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 131 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.

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

4 participants