Docs / ArchitectureKit 2.0 / Fundamentals / Defining Queries

Defining Queries

A query describes what someone wants to know. Define it as a struct, and answer it with a function that reads a view.

To filter, order, page, and transform the items, use the query package:

Changed in 2.0

import "github.com/thenativeweb/architecturekit-golang/v2/architecturekit/query"

To turn the items into a slice, call the Collect function. If the view fails while it is read, Collect returns its error instead:

type ListBooks struct {
  OnlyAvailable bool
  Limit         int
}

func listBooks(catalog architecturekit.View[BookItem]) func(context.Context, ListBooks) ([]BookItem, error) {
  return func(ctx context.Context, q ListBooks) ([]BookItem, error) {
    items := catalog.All(ctx)

    // ...

    return query.Collect(items)
  }
}

All functions of the query package take an iterator over items and errors, as All returns it, and those that return items return such an iterator again, so they can be combined without collecting anything in between. An error of the view ends the iterator. Every function hands it on unchanged, without calling the function you hand over for it, up to the function that returns the result, such as Collect, which returns it.

Note that Collect returns no items along with an error, not even the ones it read before, so that a part of the result is never taken for all of it.

Note that the query package is for reading a view. Once the items are in a slice, for example because an answer groups the same snapshot of a view in several ways, work on the slice with the slices package and a loop, such as slices.SortStableFunc to order it. A slice can not fail, so handing it to the query package would only add errors that never occur.

Filtering and Transforming Items#

To keep only some items, call the Where function with a function that selects them:

if q.OnlyAvailable {
  items = query.Where(items, func(item BookItem) bool {
    return !item.IsBorrowed
  })
}

To turn every item into something else, call the Select function:

titles := query.Select(items, func(item BookItem) string {
  return item.Title
})

Ordering Items#

To order items by a key, call the OrderBy function, or the OrderByDescending function for the reverse order:

items = query.OrderBy(items, func(item BookItem) string {
  return item.Title
})

To order items by a comparison function, for example by several fields, call the OrderByFunc function. It expects the same kind of function as slices.SortFunc, so cmp.Or and strings.Compare work with it:

items = query.OrderByFunc(items, func(left, right BookItem) int {
  return cmp.Or(
    strings.Compare(left.Author, right.Author),
    strings.Compare(left.Title, right.Title),
  )
})

Note that ordering reads all items before it hands out the first one, so if the view fails, it hands out the error alone. Ordering is stable, so items that compare as equal keep their order.

Paging Items#

To skip a number of items, call the Skip function. To stop after a number of items, call the Take function:

if q.Limit > 0 {
  items = query.Take(items, q.Limit)
}

Both can be combined, for example to get the third page of ten items:

items = query.Take(query.Skip(items, 20), 10)

Getting a Single Item#

To get the first item, call the First function. It returns false if there is none:

first, ok, err := query.First(items)

To get the only item, call the Single function. It returns query.ErrNoItems if there is none, and query.ErrTooManyItems if there are several:

type GetBook struct {
  BookID string
}

func getBook(catalog architecturekit.View[BookItem]) func(context.Context, GetBook) (BookItem, error) {
  return func(ctx context.Context, q GetBook) (BookItem, error) {
    return query.Single(query.Where(catalog.All(ctx), func(item BookItem) bool {
      return item.ID == q.BookID
    }))
  }
}

Both stop reading as soon as they know the answer, and return an error of the view only if it comes before that. First reads no further than the first item, so it never sees an error that comes later. Single reads up to the second item, and only knows that an item is the only one once the view has ended, so it also returns an error that comes after the first item. With an error, the item is empty.

Note that Where asks about every item it is handed. To get an item by its key, call the Get function of a KeyedView instead, and to get items by the value of a secondary index, call the Lookup function of the index (see Defining Views).

Counting Items#

To count items, call the Count function. To check whether at least one item matches, call the Any function, which stops at the first match:

count, err := query.Count(items)

hasBorrowedBooks, err := query.Any(items, func(item BookItem) bool {
  return item.IsBorrowed
})

Count reads all items, and if the view fails, it returns the error with 0. Any returns an error only if the view fails before the first match.