4cddfcf2f3
Backend Tests / Tests (other) (push) Failing after 1s
Backend Tests / Static Checks (push) Failing after 2s
Backend Tests / Tests (internal) (push) Failing after 2s
Backend Tests / Tests (server) (push) Failing after 1s
Backend Tests / Tests (store) (push) Failing after 1s
Build Canary Image / build-frontend (push) Failing after 1s
Frontend Tests / Lint (push) Failing after 1s
Build Canary Image / build-push (linux/amd64) (push) Has been skipped
Build Canary Image / build-push (linux/arm64) (push) Has been skipped
Build Canary Image / merge (push) Has been skipped
Frontend Tests / Build (push) Failing after 1s
Release Please / release-please (push) Failing after 0s
Proto Linter / Lint Protos (push) Failing after 2s
125 lines
3.2 KiB
Go
125 lines
3.2 KiB
Go
package filter
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/cel-go/cel"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// Engine parses CEL filters into a dialect-agnostic condition tree.
|
|
type Engine struct {
|
|
schema Schema
|
|
env *cel.Env
|
|
// nowFunc resolves the value of the `now` variable. It is frozen once per
|
|
// Compile so a single filter sees a single instant, and is overridable in
|
|
// tests for deterministic folding.
|
|
nowFunc func() time.Time
|
|
}
|
|
|
|
// NewEngine builds a new Engine for the provided schema.
|
|
func NewEngine(schema Schema) (*Engine, error) {
|
|
env, err := cel.NewEnv(schema.EnvOptions...)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to create CEL environment")
|
|
}
|
|
return &Engine{
|
|
schema: schema,
|
|
env: env,
|
|
nowFunc: time.Now,
|
|
}, nil
|
|
}
|
|
|
|
// Program stores a compiled filter condition.
|
|
type Program struct {
|
|
schema Schema
|
|
condition Condition
|
|
}
|
|
|
|
// ConditionTree exposes the underlying condition tree.
|
|
func (p *Program) ConditionTree() Condition {
|
|
return p.condition
|
|
}
|
|
|
|
// Compile parses the filter string into an executable program.
|
|
func (e *Engine) Compile(_ context.Context, filter string) (*Program, error) {
|
|
if strings.TrimSpace(filter) == "" {
|
|
return nil, errors.New("filter expression is empty")
|
|
}
|
|
|
|
ast, issues := e.env.Compile(filter)
|
|
if issues != nil && issues.Err() != nil {
|
|
return nil, errors.Wrap(issues.Err(), "failed to compile filter")
|
|
}
|
|
parsed, err := cel.AstToParsedExpr(ast)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to convert AST")
|
|
}
|
|
|
|
cond, err := buildCondition(parsed.GetExpr(), parseContext{schema: e.schema, now: e.nowFunc()})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Program{
|
|
schema: e.schema,
|
|
condition: cond,
|
|
}, nil
|
|
}
|
|
|
|
// CompileToStatement compiles and renders the filter in a single step.
|
|
func (e *Engine) CompileToStatement(ctx context.Context, filter string, opts RenderOptions) (Statement, error) {
|
|
program, err := e.Compile(ctx, filter)
|
|
if err != nil {
|
|
return Statement{}, err
|
|
}
|
|
return program.Render(opts)
|
|
}
|
|
|
|
// RenderOptions configure SQL rendering.
|
|
type RenderOptions struct {
|
|
Dialect DialectName
|
|
PlaceholderOffset int
|
|
DisableNullChecks bool
|
|
}
|
|
|
|
// Statement contains the rendered SQL fragment and its args.
|
|
type Statement struct {
|
|
SQL string
|
|
Args []any
|
|
}
|
|
|
|
// Render converts the program into a dialect-specific SQL fragment.
|
|
func (p *Program) Render(opts RenderOptions) (Statement, error) {
|
|
renderer := newRenderer(p.schema, opts)
|
|
return renderer.Render(p.condition)
|
|
}
|
|
|
|
var (
|
|
defaultOnce sync.Once
|
|
defaultInst *Engine
|
|
defaultErr error
|
|
defaultAttachmentOnce sync.Once
|
|
defaultAttachmentInst *Engine
|
|
defaultAttachmentErr error
|
|
)
|
|
|
|
// DefaultEngine returns the process-wide memo filter engine.
|
|
func DefaultEngine() (*Engine, error) {
|
|
defaultOnce.Do(func() {
|
|
defaultInst, defaultErr = NewEngine(NewSchema())
|
|
})
|
|
return defaultInst, defaultErr
|
|
}
|
|
|
|
// DefaultAttachmentEngine returns the process-wide attachment filter engine.
|
|
func DefaultAttachmentEngine() (*Engine, error) {
|
|
defaultAttachmentOnce.Do(func() {
|
|
defaultAttachmentInst, defaultAttachmentErr = NewEngine(NewAttachmentSchema())
|
|
})
|
|
return defaultAttachmentInst, defaultAttachmentErr
|
|
}
|