Ctrl K
ComunidadGuías…

filter/engine.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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.
// maxFilterExpressionSize bounds a filter expression in code points. Every
// term compiles to its own SQL predicate, so the expression length is what
// bounds the query the database has to run.
const maxFilterExpressionSize = 8 << 10

func NewEngine(schema Schema) (*Engine, error) {
	options := append([]cel.EnvOption{cel.ParserExpressionSizeLimit(maxFilterExpressionSize)}, schema.EnvOptions...)
	env, err := cel.NewEnv(options...)
	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
}