sift

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 30, 2026 License: MIT Imports: 5 Imported by: 4

README

Sift

A universal query library for Go that lets you write filter, sort, and pagination logic once and use it across multiple backends.

Go Reference Go Report Card CI

The Problem

When building applications with multiple data backends (DynamoDB, SQL, MongoDB, Elasticsearch), you end up writing the same filtering, sorting, and pagination logic multiple times in different query languages. This leads to:

  • Code duplication across data access layers
  • Inconsistent query capabilities between backends
  • Difficulty switching or adding new backends
  • Complex translation logic scattered throughout the codebase

The Solution

Sift provides a universal query expression language using an Abstract Syntax Tree (AST). Define your filters, sorts, and pagination once, then implement backend-specific evaluators to translate them into native queries.

// Define query components once
filter := sift.Eq("status", "active").And(sift.Gt("age", 18))
sort := sift.Sort("created_at", sift.SortDesc)
page := sift.Paginate().Size(20).Number(2)

// Use with any backend
adapter := mybackend.NewAdapter()
err := sift.Thru(ctx, adapter,
    sift.WithFilter(filter),
    sift.WithSort(sort),
    sift.WithPagination(page))

Installation

go get github.com/nisimpson/sift
Backend Adapters
  • DynamoDB - go get github.com/nisimpson/sift/thru/dynamodb
  • SQL - go get github.com/nisimpson/sift/thru/sql (PostgreSQL, MySQL, SQLite, SQL Server)

Quick Start

Unified API

Sift uses a unified API with options for all query operations:

import "github.com/nisimpson/sift"

// Create filter, sort, and pagination
filter := sift.Eq("status", "active").And(sift.Gt("age", 18))
sort := sift.Sort("created_at", sift.SortDesc)
page := sift.Paginate().Size(20).Number(1)

// Apply all at once
adapter := sql.NewAdapter()
err := sift.Thru(ctx, adapter,
    sift.WithFilter(filter),
    sift.WithSort(sort),
    sift.WithPagination(page))

// Or apply individually
err := sift.Thru(ctx, adapter, sift.WithFilter(filter))
err = sift.Thru(ctx, adapter, sift.WithSort(sort))
err = sift.Thru(ctx, adapter, sift.WithPagination(page))
1. Filtering
// Simple condition: status = "active"
filter := sift.Eq("status", "active")

// Complex filter: (status = "active" AND age > 18) OR role = "admin"
filter := sift.Eq("status", "active").
    And(sift.Gt("age", 18)).
    Or(sift.Eq("role", "admin"))
Expression Builder

The fluent builder API provides a readable way to construct filters:

// All comparison operations
sift.Eq("field", "value")           // Equal
sift.Neq("field", "value")          // Not equal
sift.Lt("field", 18)                // Less than
sift.Lte("field", 18)               // Less than or equal
sift.Gt("field", 18)                // Greater than
sift.Gte("field", 18)               // Greater than or equal

// String operations
sift.Contains("email", "@example.com")

// Collection operations
sift.In("status", "active")
sift.Between("age", 18, 65)

// Existence operations
sift.Exists("email")
sift.NotExists("deleted_at")

// Combine with logical operations
filter := sift.Eq("status", "active").
    And(sift.Gt("age", 18)).
    Or(sift.Eq("role", "admin"))
2. Sorting
// Sort by a single field
sort := sift.Sort("created_at", sift.SortDesc)

// Sort by multiple fields
sort := sift.Sort("created_at", sift.SortDesc).
    ThenBy("name", sift.SortAsc)

// Control NULL ordering
sort := sift.Sort("email", sift.SortAsc).NullsLast()
3. Pagination

Sift supports both offset-based and cursor-based pagination:

// Offset-based (SQL databases)
page := sift.Paginate().Size(20).Number(2)  // Page 2, 20 items per page

// Cursor-based (DynamoDB, GraphQL APIs)
page := sift.Paginate().Size(20).Cursor("token123")
4. Implement an Adapter
type MyAdapter struct {
    query string
    args  []interface{}
}

func NewAdapter() *MyAdapter {
    return &MyAdapter{
        args: make([]interface{}, 0),
    }
}

// Implement the Adapter interface
func (a *MyAdapter) Evaluator(ctx context.Context) *sift.Evaluator {
    return &sift.Evaluator{
        ConditionEvaluator:        a,
        AndEvaluator:              a,
        OrEvaluator:               a,
        NotEvaluator:              a,
        SortListEvaluator:         a,  // Optional: for sorting support
        OffsetPaginationEvaluator: a,  // Optional: for pagination support
    }
}

func (a *MyAdapter) EvaluateCondition(ctx context.Context, node *sift.Condition) error {
    switch node.Operation {
    case sift.OperationEQ:
        a.query = fmt.Sprintf("%s = ?", node.Name)
        a.args = append(a.args, node.Value)
    case sift.OperationGT:
        a.query = fmt.Sprintf("%s > ?", node.Name)
        a.args = append(a.args, node.Value)
    // ... handle other operations
    default:
        return sift.ErrorOperationNotSupported(string(node.Operation))
    }
    return nil
}

func (a *MyAdapter) EvaluateAnd(ctx context.Context, node *sift.AndOperation) error {
    leftAdapter := NewAdapter()
    if err := sift.Thru(ctx, leftAdapter, sift.WithFilter(node.Left)); err != nil {
        return err
    }
    
    rightAdapter := NewAdapter()
    if err := sift.Thru(ctx, rightAdapter, sift.WithFilter(node.Right)); err != nil {
        return err
    }
    
    a.query = fmt.Sprintf("(%s) AND (%s)", leftAdapter.query, rightAdapter.query)
    a.args = append(leftAdapter.args, rightAdapter.args...)
    return nil
}

// Implement EvaluateOr, EvaluateNot, EvaluateSortList, EvaluateOffsetPagination...
5. Use the Adapter
filter := sift.Eq("status", "active")
sort := sift.Sort("created_at", sift.SortDesc)
page := sift.Paginate().Size(20).Number(1)

adapter := NewAdapter()
err := sift.Thru(ctx, adapter,
    sift.WithFilter(filter),
    sift.WithSort(sort),
    sift.WithPagination(page))

if err != nil {
    log.Fatal(err)
}

// Use the generated query
query := fmt.Sprintf("SELECT * FROM users WHERE %s ORDER BY %s LIMIT %d OFFSET %d",
    adapter.Query(), adapter.OrderBy(), adapter.Limit(), adapter.Offset())
rows, err := db.Query(query, adapter.Args()...)

Supported Operations

Condition Operations
Category Constant Description Example
Comparison OperationEQ Equal eq(status,active)
OperationNEQ Not equal ne(status,deleted)
OperationLT Less than lt(age,18)
OperationLTE Less than or equal le(age,18)
OperationGT Greater than gt(age,18)
OperationGTE Greater than or equal ge(age,18)
String OperationContains Substring match contains(email,@example.com)
OperationBeginsWith Prefix match begins_with(name,John)
Collection OperationIn Value in list in(status,active,pending)
Existence OperationExists Attribute exists exists(email)
OperationNotExists Attribute doesn't exist not_exists(deleted_at)
Range OperationBetween Value between bounds between(age,18,65)
Logical Operations
Type Description Example
AndOperation Logical AND - combines two conditions and(eq(status,active),gt(age,18))
OrOperation Logical OR - either condition matches or(eq(role,admin),eq(role,moderator))
NotOperation Logical NOT - negates a condition not(eq(deleted,true))

Serialization

Sift includes built-in serialization to a URL-safe prefix notation format:

// Serialize (no custom expressions)
filter := &sift.Condition{
    Name:      "status",
    Operation: sift.OperationEQ,
    Value:     "active",
}
str, err := sift.Format(filter, nil)
// str = "eq(status,active)"

// Deserialize (no custom expressions)
expr, err := sift.Parse("and(eq(status,active),gt(age,18))", nil)

// With custom expressions, provide a registry
registry := dynamodb.NewRegistry()
filter := dynamodb.Size("tags", sift.OperationGT, 5)
str, err := sift.Format(filter, registry)
// str = "size(tags,gt,5)"

parsed, err := sift.Parse(str, registry)
Format Examples
eq(status,active)                     // status = "active"
gt(age,18)                            // age > 18
contains(email,@example.com)          // email contains "@example.com"
and(eq(status,active),gt(age,18))     // status = "active" AND age > 18
or(eq(role,admin),eq(role,moderator)) // role = "admin" OR role = "moderator"
not(eq(deleted,true))                 // NOT deleted = true

This format is ideal for URL query strings:

GET /users?filter=and(eq(status,active),gt(age,18))

Sorting

Sift provides sorting support through the unified Thru() API with WithSort() option.

Basic Sorting
// Sort by a single field
sort := sift.Sort("created_at", sift.SortDesc)

adapter := sql.NewAdapter()
sift.Thru(ctx, adapter, sift.WithSort(sort))

query := fmt.Sprintf("SELECT * FROM users ORDER BY %s", adapter.OrderBy())
// SELECT * FROM users ORDER BY created_at DESC
Multiple Sort Fields

Use ThenBy() to add additional sort fields:

// Sort by created_at DESC, then by name ASC
sort := sift.Sort("created_at", sift.SortDesc).
    ThenBy("name", sift.SortAsc)

adapter := sql.NewAdapter()
sift.Thru(ctx, adapter, sift.WithSort(sort))
// ORDER BY created_at DESC, name ASC
NULL Handling

Control where NULL values appear in the sort order:

// NULLs appear last in ascending sort
sort := sift.Sort("email", sift.SortAsc).NullsLast()

// Can be applied to specific fields in multi-field sorts
sort := sift.Sort("created_at", sift.SortDesc).
    ThenBy("email", sift.SortAsc).NullsLast().
    ThenBy("name", sift.SortAsc)
// Only email field has NullsLast
Combining Filtering and Sorting

Use both filtering and sorting together:

// Filter and sort
filter := sift.Eq("status", "active").And(sift.Gt("age", 18))
sort := sift.Sort("created_at", sift.SortDesc).ThenBy("name", sift.SortAsc)

adapter := sql.NewAdapter()
sift.Thru(ctx, adapter,
    sift.WithFilter(filter),
    sift.WithSort(sort))

query := fmt.Sprintf("SELECT * FROM users WHERE %s ORDER BY %s",
    adapter.Query(), adapter.OrderBy())
// SELECT * FROM users WHERE (status = $1) AND (age > $2) ORDER BY created_at DESC, name ASC
Sort Directions

Two sort directions are available:

  • sift.SortAsc - Ascending order (A-Z, 0-9, oldest-newest)
  • sift.SortDesc - Descending order (Z-A, 9-0, newest-oldest)

Pagination

Sift supports both offset-based and cursor-based pagination strategies.

Offset-Based Pagination

Used with SQL databases (LIMIT/OFFSET):

// First page (20 items)
page := sift.Paginate().Size(20).Number(1)

// Second page
page := sift.Paginate().Size(20).Number(2)

adapter := sql.NewAdapter()
sift.Thru(ctx, adapter, sift.WithPagination(page))

query := fmt.Sprintf("SELECT * FROM users LIMIT %d OFFSET %d",
    adapter.Limit(), adapter.Offset())
// SELECT * FROM users LIMIT 20 OFFSET 20
Cursor-Based Pagination

Used with DynamoDB, GraphQL, and other cursor-based APIs:

// First page
page := sift.Paginate().Size(20).Cursor("")

// Next page with cursor from previous response
page := sift.Paginate().Size(20).Cursor("token123")

adapter := dynamodb.NewAdapter()
sift.Thru(ctx, adapter, sift.WithPagination(page))

input := &dynamodb.QueryInput{
    TableName: aws.String("Users"),
    Limit:     adapter.Limit(),  // *int32
}
if token := adapter.Token(); token != "" {
    input.ExclusiveStartKey = decodeToken(token)
}
Default Limits (SQL)

For SQL databases, you can configure a default limit to prevent unbounded queries:

config := &sql.Config{
    Dialect:      sql.DialectPostgreSQL,
    DefaultLimit: 100,  // Applied when no pagination specified
}
adapter := sql.NewAdapterWithConfig(config)

// No pagination - uses default limit of 100
sift.Thru(ctx, adapter, sift.WithFilter(filter))
fmt.Println(adapter.Limit())  // 100

// With pagination - overrides default
page := sift.Paginate().Size(20).Number(1)
sift.Thru(ctx, adapter, sift.WithPagination(page))
fmt.Println(adapter.Limit())  // 20
Complete Example

Combining filter, sort, and pagination:

// SQL (offset-based)
filter := sift.Eq("status", "active").And(sift.Gt("age", 18))
sort := sift.Sort("created_at", sift.SortDesc)
page := sift.Paginate().Size(20).Number(2)

adapter := sql.NewAdapter()
sift.Thru(ctx, adapter,
    sift.WithFilter(filter),
    sift.WithSort(sort),
    sift.WithPagination(page))

query := fmt.Sprintf("SELECT * FROM users WHERE %s ORDER BY %s LIMIT %d OFFSET %d",
    adapter.Query(), adapter.OrderBy(), adapter.Limit(), adapter.Offset())
// SELECT * FROM users WHERE (status = $1) AND (age > $2) ORDER BY created_at DESC LIMIT 20 OFFSET 20

// DynamoDB (cursor-based)
filter := sift.Eq("status", "active")
sort := sift.Sort("created_at", sift.SortDesc)
page := sift.Paginate().Size(20).Cursor("token123")

adapter := dynamodb.NewAdapter()
sift.Thru(ctx, adapter,
    sift.WithFilter(filter),
    sift.WithSort(sort),
    sift.WithPagination(page))

expr, _ := adapter.Expression()
input := &dynamodb.QueryInput{
    TableName:                 aws.String("Users"),
    FilterExpression:          expr.Condition(),
    ExpressionAttributeNames:  expr.Names(),
    ExpressionAttributeValues: expr.Values(),
    ScanIndexForward:          adapter.ScanIndexForward(),
    Limit:                     adapter.Limit(),
}
if token := adapter.Token(); token != "" {
    input.ExclusiveStartKey = decodeToken(token)
}
Adapter Support

Adapters choose which pagination strategy to support:

func (a *Adapter) Evaluator(ctx context.Context) *sift.Evaluator {
    return &sift.Evaluator{
        // Filtering
        ConditionEvaluator: a,
        AndEvaluator:       a,
        // Sorting
        SortListEvaluator:  a,
        // Pagination (choose one or both)
        OffsetPaginationEvaluator: a,  // For SQL
        CursorPaginationEvaluator: a,  // For DynamoDB, GraphQL
    }
}

Custom Expressions

Extend Sift with custom expression types for backend-specific operations using the Registry pattern:

// Define a custom expression type
type GeoWithinExpression struct {
    Field  string
    Lat    float64
    Lng    float64
    Radius float64
}

func (g *GeoWithinExpression) Type() string {
    return "geo_within"
}

// Define a formatter
type GeoFormatter struct{}

func (GeoFormatter) FormatCustomExpression(expr sift.CustomExpression) (string, error) {
    g := expr.(*GeoWithinExpression)
    return fmt.Sprintf("geo_within(%s,%f,%f,%f)", 
        g.Field, g.Lat, g.Lng, g.Radius), nil
}

func (GeoFormatter) ParseCustomExpression(p *sift.Parser) (sift.CustomExpression, error) {
    field := p.ReadValue()
    p.Expect(',')
    lat, _ := strconv.ParseFloat(p.ReadValue(), 64)
    p.Expect(',')
    lng, _ := strconv.ParseFloat(p.ReadValue(), 64)
    p.Expect(',')
    radius, _ := strconv.ParseFloat(p.ReadValue(), 64)
    p.Expect(')')
    return &GeoWithinExpression{field, lat, lng, radius}, nil
}

// Create a registry and register the custom expression
registry := sift.NewRegistry()
registry.Register("geo_within", GeoFormatter{})

// Use the custom expression
expr := sift.NewCustomExpression(&GeoWithinExpression{
    Field:  "location",
    Lat:    40.7128,
    Lng:    -74.0060,
    Radius: 5000,
})

// Serialize with registry
str, _ := sift.Format(expr, registry)
// str = "geo_within(location,40.712800,-74.006000,5000.000000)"

// Parse with registry
parsed, _ := sift.Parse(str, registry)
Registry Pattern

The registry pattern prevents naming collisions when multiple backends define custom expressions:

// Each backend provides its own registry
dynamoRegistry := dynamodb.NewRegistry()  // Registers "size", "attribute_type"
exprRegistry := exprlang.NewRegistry()    // Registers "exprlang"

// Use the appropriate registry for your backend
filter := dynamodb.Size("tags", sift.OperationGT, 5)
str, _ := sift.Format(filter, dynamoRegistry)
// str = "size(tags,gt,5)"

// For expressions without custom types, pass nil
filter := sift.Eq("status", "active")
str, _ := sift.Format(filter, nil)
// str = "eq(status,active)"

Use Cases

Multi-Backend Applications
type UserRepository interface {
    Find(ctx context.Context, opts ...sift.Option) ([]*User, error)
}

// DynamoDB implementation
type DynamoUserRepo struct {
    client *dynamodb.Client
}

func (r *DynamoUserRepo) Find(ctx context.Context, opts ...sift.Option) ([]*User, error) {
    adapter := dynamodb.NewAdapter()
    sift.Thru(ctx, adapter, opts...)
    
    expr, _ := adapter.Expression()
    input := &dynamodb.QueryInput{
        TableName:                 aws.String("Users"),
        FilterExpression:          expr.Condition(),
        ExpressionAttributeNames:  expr.Names(),
        ExpressionAttributeValues: expr.Values(),
        ScanIndexForward:          adapter.ScanIndexForward(),
        Limit:                     adapter.Limit(),
    }
    // ... execute query and scan results
}

// PostgreSQL implementation
type PostgresUserRepo struct {
    db *sql.DB
}

func (r *PostgresUserRepo) Find(ctx context.Context, opts ...sift.Option) ([]*User, error) {
    adapter := sqlAdapter.NewAdapter() // Defaults to PostgreSQL
    sift.Thru(ctx, adapter, opts...)
    
    query := fmt.Sprintf("SELECT * FROM users WHERE %s ORDER BY %s LIMIT %d OFFSET %d",
        adapter.Query(), adapter.OrderBy(), adapter.Limit(), adapter.Offset())
    rows, err := r.db.Query(query, adapter.Args()...)
    // ... scan rows into users
}

// Usage - same interface for both backends
filter := sift.Eq("status", "active")
sort := sift.Sort("created_at", sift.SortDesc)
page := sift.Paginate().Size(20).Number(1)

users, err := repo.Find(ctx,
    sift.WithFilter(filter),
    sift.WithSort(sort),
    sift.WithPagination(page))
API Query Parameters
func (h *Handler) ListUsers(w http.ResponseWriter, r *http.Request) {
    // Parse query parameters
    filterStr := r.URL.Query().Get("filter")
    sortStr := r.URL.Query().Get("sort")
    pageNum, _ := strconv.Atoi(r.URL.Query().Get("page"))
    pageSize, _ := strconv.Atoi(r.URL.Query().Get("size"))
    
    // Build options
    var opts []sift.Option
    
    if filterStr != "" {
        filter, err := sift.Parse(filterStr, nil)
        if err != nil {
            http.Error(w, "Invalid filter", http.StatusBadRequest)
            return
        }
        opts = append(opts, sift.WithFilter(filter))
    }
    
    if sortStr != "" {
        // Parse sort string (e.g., "created_at:desc,name:asc")
        sort := parseSortString(sortStr)
        opts = append(opts, sift.WithSort(sort))
    }
    
    if pageSize > 0 {
        page := sift.Paginate().Size(pageSize).Number(pageNum)
        opts = append(opts, sift.WithPagination(page))
    }
    
    users, err := h.repo.Find(r.Context(), opts...)
    // ...
}
Testing
func TestUserFiltering(t *testing.T) {
    filter := sift.Eq("status", "active")
    sort := sift.Sort("created_at", sift.SortDesc)
    page := sift.Paginate().Size(20).Number(1)
    
    // Test with different backends
    t.Run("DynamoDB", func(t *testing.T) {
        adapter := dynamodb.NewAdapter()
        err := sift.Thru(ctx, adapter,
            sift.WithFilter(filter),
            sift.WithSort(sort),
            sift.WithPagination(page))
        // assertions...
    })
    
    t.Run("PostgreSQL", func(t *testing.T) {
        adapter := postgres.NewAdapter()
        err := sift.Thru(ctx, adapter,
            sift.WithFilter(filter),
            sift.WithSort(sort),
            sift.WithPagination(page))
        // assertions...
    })
}

Design Philosophy

  • Simple interfaces - Implement only what you need
  • Type-safe - Leverage Go's type system
  • Composable - Build complex filters from simple operations
  • Extensible - Add custom operations for your backend
  • Backend-agnostic - Works with any data store

Contributing

Contributions welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Development Setup
# Clone the repository
git clone https://github.com/nisimpson/sift.git
cd sift

# Install development tools
make install-tools

# Run tests
make test

# Run all checks (formatting, linting, tests)
make check
Running Tests
# Run all tests
make test

# Run tests with coverage
make test-coverage

# Run tests without race detector (faster)
make test-short

# Run benchmarks
make bench
Code Quality
# Format code
make fmt

# Run linter
make lint

# Run go vet
make vet

# Run all checks
make check
Continuous Integration

All pull requests are automatically tested with:

  • Multiple Go versions (1.21, 1.22, 1.23)
  • Race detector
  • golangci-lint
  • Formatting checks

See .github/workflows/README.md for details.

License

MIT License - see LICENSE file for details

Acknowledgments

Inspired by the need for consistent filtering across multiple data backends in modern applications.

Documentation

Overview

Package sift provides a universal filter expression language using an Abstract Syntax Tree (AST). Write filter logic once, then implement backend-specific evaluators to translate it into native queries.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnsupported indicates an operation or node type is not supported by the evaluator.
	ErrUnsupported = errors.ErrUnsupported
)

Functions

func ErrorNodeNotSupported

func ErrorNodeNotSupported[T Expression](expr T) error

ErrorNodeNotSupported returns an error indicating the expression type is not supported.

func ErrorOperationNotSupported

func ErrorOperationNotSupported(operation string) error

ErrorOperationNotSupported returns an error indicating the operation is not supported.

func Format

func Format(registry *Registry, opts ...FormatOption) (string, error)

Format serializes filter, sort, and pagination expressions to a string using prefix notation. The format is URL-safe and unambiguous, suitable for query strings.

If registry is nil, no custom expressions are supported.

Supported filter operations:

eq(field,value)          // equal
ne(field,value)          // not equal
lt(field,value)          // less than
le(field,value)          // less than or equal
gt(field,value)          // greater than
ge(field,value)          // greater than or equal
contains(field,value)    // substring match
begins_with(field,value) // prefix match
in(field,value)          // value in list
exists(field)            // field exists
not_exists(field)        // field doesn't exist
between(field,value)     // value between bounds
and(expr1,expr2)         // logical AND
or(expr1,expr2)          // logical OR
not(expr)                // logical NOT

Sort format:

field:asc                // single field ascending
field:desc               // single field descending
field:asc:nullslast      // with nulls last
field1:desc,field2:asc   // multiple fields

Pagination format:

size:20,number:2         // offset-based (page number)
size:20,cursor:token123  // cursor-based

Custom expressions registered in the registry are also supported.

Special characters (commas, parentheses, backslashes, colons) in values are escaped with backslash.

Example:

str, _ := sift.Format(registry,
    sift.WithFilter(filterExpr),
    sift.WithSort(sortExpr),
    sift.WithPagination(pageExpr))
// Output: filter(and(eq(status,active),gt(age,18))),sort(created_at:desc,name:asc),page(size:20,number:2)

func FormatFilter

func FormatFilter(expr Expression, registry *Registry) (string, error)

FormatFilter serializes a filter expression to a string using prefix notation. This is a convenience function for formatting only a filter expression.

Example:

str, _ := sift.FormatFilter(filterExpr, registry)
// Output: and(eq(status,active),gt(age,18))

func Thru

func Thru(ctx context.Context, adapter Adapter, options ...Option) error

Thru evaluates query options using the provided adapter. This is the main entry point for processing filters, sorts, and pagination.

Example:

sift.Thru(ctx, adapter,
    sift.WithFilter(filter),
    sift.WithSort(sort),
    sift.WithPagination(page))

Types

type Adapter

type Adapter interface {
	// Evaluator returns an evaluator configured for the specific backend.
	// The evaluator should have the appropriate interface implementations set
	// based on what operations the backend supports.
	Evaluator(ctx context.Context) *Evaluator
}

Adapter provides the bridge between the sift filter AST and backend-specific implementations. Backends implement this interface to provide their own evaluator that can translate filter expressions into native queries or operations.

type AndEvaluator

type AndEvaluator interface {
	EvaluateAnd(ctx context.Context, node *AndOperation) error
}

AndEvaluator evaluates AND operations. Implement this interface to support logical AND in your backend.

type AndOperation

type AndOperation struct {
	Left  Expression
	Right Expression
}

AndOperation represents a logical AND between two filter expressions.

func (AndOperation) String

func (a AndOperation) String() string

String returns the string representation of the AND operation.

type Condition

type Condition struct {
	Name      string    // attribute name
	Operation Operation // operation type (use Operation constants)
	Value     string    // comparison value
}

Condition represents a single filter condition (e.g., "status = active").

func (*Condition) And

func (c *Condition) And(expr Expression) ExpressionBuilder

And creates a logical AND operation between this condition and another expression. Returns a Builder that can be used to chain additional operations.

func (*Condition) Not

func (c *Condition) Not() ExpressionBuilder

Not creates a logical NOT operation, negating this condition. Returns an ExpressionBuilder that can be used to chain additional operations.

func (*Condition) Or

Or creates a logical OR operation between this condition and another expression. Returns a Builder that can be used to chain additional operations.

func (Condition) String

func (c Condition) String() string

String returns the string representation of the condition in the format "operation(name,value)".

type ConditionEvaluator

type ConditionEvaluator interface {
	EvaluateCondition(ctx context.Context, node *Condition) error
}

ConditionEvaluator evaluates single filter conditions. Implement this interface to handle basic comparisons in your backend.

type CursorPagination

type CursorPagination struct {
	Size   int    // Number of items per page
	Cursor string // Opaque cursor token for the next page
}

CursorPagination represents cursor-based pagination (cursor + size). This is commonly used with DynamoDB, GraphQL, and other cursor-based APIs.

type CursorPaginationEvaluator

type CursorPaginationEvaluator interface {
	EvaluateCursorPagination(ctx context.Context, page *CursorPagination) error
}

CursorPaginationEvaluator evaluates cursor-based pagination.

type CustomEvaluator

type CustomEvaluator interface {
	EvaluateCustom(ctx context.Context, node CustomExpression) error
}

CustomEvaluator evaluates custom node types. Implement this interface to support backend-specific operations.

type CustomExpression

type CustomExpression interface {
	fmt.Stringer
	Type() string
}

CustomExpression allows backends to define custom node types beyond the standard operations.

type CustomFormatter

type CustomFormatter interface {
	// FormatCustomExpression serializes a custom expression to a string.
	FormatCustomExpression(expression CustomExpression) (string, error)

	// ParseCustomExpression deserializes a string to a [CustomExpression].
	// The parser has already consumed the function name and opening parenthesis.
	// The parser should consume up to and including the closing parenthesis.
	ParseCustomExpression(p *Parser) (CustomExpression, error)
}

CustomFormatter handles serialization and deserialization of CustomExpression types.

type Evaluator

Evaluator holds the interface implementations for evaluating filter nodes. Backend implementations should embed this in their evaluator types and set only the interfaces they support.

type Expression

type Expression interface {
	fmt.Stringer
	// contains filtered or unexported methods
}

Expression represents an expression in the filter AST.

func NewCustomExpression

func NewCustomExpression(custom CustomExpression) Expression

NewCustomExpression wraps a custom node implementation to make it compatible with the Expression interface. This allows backends to define their own node types while integrating with the standard AST.

type ExpressionBuilder

type ExpressionBuilder struct {
	// contains filtered or unexported fields
}

ExpressionBuilder provides a fluent interface for constructing complex filter expressions. It wraps an Expression and allows chaining of logical operations like And, Or, and Not.

func Between

func Between[T comparable](field string, minVal, maxVal T) ExpressionBuilder

Between creates a new ExpressionBuilder for range checking. The resulting expression will be true if the field value is between minVal and maxVal (inclusive).

func Contains

func Contains[T comparable](field string, val T) ExpressionBuilder

Contains creates a new ExpressionBuilder for a containment check. The resulting expression will be true if the field value contains the given value.

func Eq

func Eq[T comparable](field string, val T) ExpressionBuilder

Eq creates a new ExpressionBuilder for an equality comparison. The resulting expression will be true if the field value equals the given value.

func Exists

func Exists(field string) ExpressionBuilder

Exists creates a new ExpressionBuilder that checks if a field exists. The resulting expression will be true if the field is present.

func Gt

func Gt[T comparable](field string, val T) ExpressionBuilder

Gt creates a new ExpressionBuilder for a "greater than" comparison. The resulting expression will be true if the field value is greater than the given value.

func Gte

func Gte[T comparable](field string, val T) ExpressionBuilder

Gte creates a new ExpressionBuilder for a "greater than or equal" comparison. The resulting expression will be true if the field value is greater than or equal to the given value.

func In

func In[T comparable](field string, val T) ExpressionBuilder

In creates a new ExpressionBuilder for membership testing. The resulting expression will be true if the field value is in the given collection.

func Lt

func Lt[T comparable](field string, val T) ExpressionBuilder

Lt creates a new ExpressionBuilder for a "less than" comparison. The resulting expression will be true if the field value is less than the given value.

func Lte

func Lte[T comparable](field string, val T) ExpressionBuilder

Lte creates a new ExpressionBuilder for a "less than or equal" comparison. The resulting expression will be true if the field value is less than or equal to the given value.

func Neq

func Neq[T comparable](field string, val T) ExpressionBuilder

Neq creates a new ExpressionBuilder for a "not equal" comparison. The resulting expression will be true if the field value does not equal the given value.

func NewExpressionBuilder

func NewExpressionBuilder(expr Expression) ExpressionBuilder

NewExpressionBuilder creates a new ExpressionBuilder that wraps the given expression. This allows the expression to be used with the fluent interface for chaining logical operations like And, Or, and Not.

func NotExists

func NotExists(field string) ExpressionBuilder

NotExists creates a new ExpressionBuilder that checks if a field does not exist. The resulting expression will be true if the field is not present.

func (ExpressionBuilder) And

And creates a new Builder that represents the logical AND of this Builder and the given expression. The resulting expression will be true only if both operands evaluate to true.

func (ExpressionBuilder) Not

Not creates a new Builder that represents the logical NOT of this Builder. The resulting expression will be true if this Builder evaluates to false, and vice versa.

func (ExpressionBuilder) Or

Or creates a new Builder that represents the logical OR of this Builder and the given expression. The resulting expression will be true if either operand evaluates to true.

func (ExpressionBuilder) String

func (b ExpressionBuilder) String() string

String implements the fmt.Stringer interface by delegating to the wrapped expression.

type FormatOption

type FormatOption interface {
	Option // Embed Option interface
	// contains filtered or unexported methods
}

FormatOption configures what to include in the formatted output. It also implements Option so it can be used with Thru().

func WithFilter

func WithFilter(expr Expression) FormatOption

WithFilter adds a filter expression. It can be used with both Format() and Thru().

Example:

// With Format
str, _ := sift.Format(registry, sift.WithFilter(filterExpr))

// With Thru
sift.Thru(ctx, adapter, sift.WithFilter(filterExpr))

func WithPagination

func WithPagination(expr PaginationExpression) FormatOption

WithPagination adds a pagination expression. It can be used with both Format() and Thru().

Example:

// With Format
str, _ := sift.Format(registry, sift.WithPagination(pageExpr))

// With Thru
sift.Thru(ctx, adapter, sift.WithPagination(pageExpr))

func WithSort

func WithSort(expr SortExpression) FormatOption

WithSort adds a sort expression. It can be used with both Format() and Thru().

Example:

// With Format
str, _ := sift.Format(registry, sift.WithSort(sortExpr))

// With Thru
sift.Thru(ctx, adapter, sift.WithSort(sortExpr))

type NotEvaluator

type NotEvaluator interface {
	EvaluateNot(ctx context.Context, node *NotOperation) error
}

NotEvaluator evaluates NOT operations. Implement this interface to support logical negation in your backend.

type NotOperation

type NotOperation struct {
	Child Expression
}

NotOperation represents a logical NOT (negation) of a filter expression.

func (NotOperation) String

func (n NotOperation) String() string

String returns the string representation of the NOT operation.

type OffsetPagination

type OffsetPagination struct {
	Size   int // Number of items per page
	Number int // Page number (1-based)
}

OffsetPagination represents offset-based pagination (page number + size). This is commonly used with SQL databases using LIMIT/OFFSET.

type OffsetPaginationEvaluator

type OffsetPaginationEvaluator interface {
	EvaluateOffsetPagination(ctx context.Context, page *OffsetPagination) error
}

OffsetPaginationEvaluator evaluates offset-based pagination.

type Operation

type Operation string

Operation defines the type of comparison or logical operation.

const (
	OperationEQ         Operation = "eq"          // equal
	OperationNEQ        Operation = "ne"          // not equal
	OperationLT         Operation = "lt"          // less than
	OperationLTE        Operation = "le"          // less than or equal
	OperationGT         Operation = "gt"          // greater than
	OperationGTE        Operation = "ge"          // greater than or equal
	OperationContains   Operation = "contains"    // substring match
	OperationBeginsWith Operation = "begins_with" // prefix match
	OperationIn         Operation = "in"          // value in list
	OperationExists     Operation = "exists"      // attribute exists
	OperationNotExists  Operation = "not_exists"  // attribute doesn't exist
	OperationBetween    Operation = "between"     // value between two bounds
)

Operation constants define the supported comparison and existence operations.

func (Operation) IsCondition

func (o Operation) IsCondition() bool

IsCondition returns true if the operation is a condition operation that requires a value. Condition operations include equality, inequality, relational, and pattern matching operations.

func (Operation) IsExistence

func (o Operation) IsExistence() bool

IsExistence returns true if the operation is an existence check that only tests for the presence or absence of an attribute without comparing values.

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option represents a query option that can be applied to an adapter. Options include filtering, sorting, and pagination. FormatOption is a superset that also supports formatting.

type OrEvaluator

type OrEvaluator interface {
	EvaluateOr(ctx context.Context, node *OrOperation) error
}

OrEvaluator evaluates OR operations. Implement this interface to support logical OR in your backend.

type OrOperation

type OrOperation struct {
	Left  Expression
	Right Expression
}

OrOperation represents a logical OR between two filter expressions.

func (OrOperation) String

func (o OrOperation) String() string

String returns the string representation of the OR operation.

type PaginationBuilder

type PaginationBuilder struct {
	// contains filtered or unexported fields
}

PaginationBuilder provides a fluent interface for constructing pagination expressions.

func Paginate

func Paginate() PaginationBuilder

Paginate creates a new PaginationBuilder. Use Size() to set the page size, then either Number() for offset-based or Cursor() for cursor-based pagination.

Example (offset-based):

page := sift.Paginate().Size(20).Number(2)

Example (cursor-based):

page := sift.Paginate().Size(20).Cursor("token123")

func (PaginationBuilder) Cursor

Cursor sets the cursor token for cursor-based pagination. This creates a CursorPagination expression. If Number() was previously called, this overrides it.

func (PaginationBuilder) Number

func (p PaginationBuilder) Number(number int) PaginationExpression

Number sets the page number for offset-based pagination. This creates an OffsetPagination expression. If Cursor() was previously called, this overrides it.

func (PaginationBuilder) Size

Size sets the page size (number of items per page).

type PaginationExpression

type PaginationExpression interface {
	// contains filtered or unexported methods
}

PaginationExpression represents a pagination expression in the AST.

type Parser

type Parser struct {
	// contains filtered or unexported fields
}

Parser is a recursive descent parser for prefix notation filter expressions. It is exported to allow CustomFormatter parsers to access parsing utilities.

func (*Parser) Expect

func (p *Parser) Expect(ch byte) bool

Expect checks if the current character matches the expected character. If it matches, advances the position and returns true. This method is exported for use by CustomFormatter parsers.

func (*Parser) ReadValue

func (p *Parser) ReadValue() string

ReadValue reads a field name or value, handling escape sequences. Stops at unescaped commas or closing parentheses. This method is exported for use by custom expression parsers.

func (*Parser) SkipWhitespace

func (p *Parser) SkipWhitespace()

SkipWhitespace advances the position past any whitespace characters. This method is exported for use by CustomFormatter parsers.

type Query

type Query struct {
	Filter     Expression
	Sort       SortExpression
	Pagination PaginationExpression
}

Query represents a complete query with filter, sort, and pagination.

func ParseQuery

func ParseQuery(s string, registry *Registry) (*Query, error)

ParseQuery deserializes a complete query string into filter, sort, and pagination expressions. The input should be in the format produced by Format() with multiple options.

Format: filter(...),sort(...),page(...) Any component can be omitted.

Example:

query, _ := sift.ParseQuery("filter(eq(status,active)),sort(created_at:desc),page(size:20,number:2)", registry)
// query.Filter, query.Sort, query.Pagination are all populated

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry holds custom expression formatters. Use NewRegistry to create a registry and Register to add formatters.

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates a new custom expression registry.

func (*Registry) Register

func (r *Registry) Register(name string, formatter CustomFormatter)

Register registers a custom formatter by name. The name is what appears in the serialized format (e.g., "size", "geo_within"). Panics if a formatter with the same name is already registered.

Example:

registry := sift.NewRegistry()
registry.Register("size", SizeFormatter{})

type SortBuilder

type SortBuilder struct {
	// contains filtered or unexported fields
}

SortBuilder provides a fluent API for building sort expressions.

func Sort

func Sort(name string, direction SortDirection) SortBuilder

Sort creates a new sort expression with a single field.

Example:

sort := sift.Sort("created_at", sift.SortDesc)

func (SortBuilder) NullsLast

func (b SortBuilder) NullsLast() SortBuilder

NullsLast sets the NullsLast flag on the most recently added field. This controls whether NULL values appear last in the sort order. Note: Not all backends support this feature.

Example:

sort := sift.Sort("email", sift.SortAsc).NullsLast()

func (SortBuilder) ThenBy

func (b SortBuilder) ThenBy(name string, direction SortDirection) SortBuilder

ThenBy adds another sort field to the expression. Fields are applied in the order they are added.

Example:

sort := sift.Sort("created_at", sift.SortDesc).ThenBy("name", sift.SortAsc)

type SortDirection

type SortDirection string

SortDirection represents the direction of sorting.

const (
	// SortAsc sorts in ascending order (A-Z, 0-9, oldest-newest).
	SortAsc SortDirection = "asc"
	// SortDesc sorts in descending order (Z-A, 9-0, newest-oldest).
	SortDesc SortDirection = "desc"
)

type SortExpression

type SortExpression interface {
	// contains filtered or unexported methods
}

SortExpression represents a sorting expression in the AST.

type SortField

type SortField struct {
	Name      string
	Direction SortDirection
	NullsLast bool // Control NULL ordering (backend-specific support)
}

SortField represents a single field to sort by.

func (SortField) ThenBy

func (s SortField) ThenBy(name string, direction SortDirection) SortBuilder

ThenBy creates a new SortBuilder starting with this field and adding another field. This allows chaining sort operations starting from a SortField.

Example:

field := &sift.SortField{Name: "created_at", Direction: sift.SortDesc}
sort := field.ThenBy("name", sift.SortAsc)

type SortFieldEvaluator

type SortFieldEvaluator interface {
	EvaluateSortField(ctx context.Context, field *SortField) error
}

SortFieldEvaluator evaluates a single sort field.

type SortList

type SortList struct {
	Fields []*SortField
}

SortList represents multiple sort fields applied in order.

type SortListEvaluator

type SortListEvaluator interface {
	EvaluateSortList(ctx context.Context, list *SortList) error
}

SortListEvaluator evaluates a list of sort fields.

Directories

Path Synopsis
thru
dynamodb module
exprlang module
jsonapi module
sql module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL