|
|
|
|
|
# Memo Filter Engine
|
|
|
|
|
|
|
|
|
|
|
|
This package houses the memo-only filter engine that turns CEL expressions into
|
|
|
|
|
|
SQL fragments. The engine follows a three phase pipeline inspired by systems
|
|
|
|
|
|
such as Calcite or Prisma:
|
|
|
|
|
|
|
|
|
|
|
|
1. **Parsing** – CEL expressions are parsed with `cel-go` and validated against
|
|
|
|
|
|
the memo-specific environment declared in `schema.go`. Only fields that
|
|
|
|
|
|
exist in the schema can surface in the filter.
|
|
|
|
|
|
2. **Normalization** – the raw CEL AST is converted into an intermediate
|
|
|
|
|
|
representation (IR) defined in `ir.go`. The IR is a dialect-agnostic tree of
|
|
|
|
|
|
conditions (logical operators, comparisons, list membership, etc.). This
|
|
|
|
|
|
step enforces schema rules (e.g. operator compatibility, type checks).
|
|
|
|
|
|
3. **Rendering** – the renderer in `render.go` walks the IR and produces a SQL
|
|
|
|
|
|
fragment plus placeholder arguments tailored to a target dialect
|
|
|
|
|
|
(`sqlite`, `mysql`, or `postgres`). Dialect differences such as JSON access,
|
|
|
|
|
|
boolean semantics, placeholders, and `LIKE` vs `ILIKE` are encapsulated in
|
|
|
|
|
|
renderer helpers.
|
|
|
|
|
|
|
|
|
|
|
|
The entry point is `filter.DefaultEngine()` from `engine.go`. It lazily constructs
|
|
|
|
|
|
an `Engine` configured with the memo schema and exposes:
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
engine, _ := filter.DefaultEngine()
|
|
|
|
|
|
stmt, _ := engine.CompileToStatement(ctx, `has_task_list && visibility == "PUBLIC"`, filter.RenderOptions{
|
|
|
|
|
|
Dialect: filter.DialectPostgres,
|
|
|
|
|
|
})
|
|
|
|
|
|
// stmt.SQL -> "((memo.payload->'property'->>'hasTaskList')::boolean IS TRUE AND memo.visibility = $1)"
|
|
|
|
|
|
// stmt.Args -> ["PUBLIC"]
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## Core Files
|
|
|
|
|
|
|
|
|
|
|
|
| File | Responsibility |
|
|
|
|
|
|
| ------------- | ------------------------------------------------------------------------------- |
|
|
|
|
|
|
| `schema.go` | Declares memo fields, their types, backing columns, CEL environment options |
|
|
|
|
|
|
| `ir.go` | IR node definitions used across the pipeline |
|
|
|
|
|
|
| `parser.go` | Converts CEL `Expr` into IR while applying schema validation |
|
|
|
|
|
|
| `render.go` | Translates IR into SQL, handling dialect-specific behavior |
|
|
|
|
|
|
| `engine.go` | Glue between the phases; exposes `Compile`, `CompileToStatement`, and `DefaultEngine` |
|
|
|
|
|
|
| `helpers.go` | Convenience helpers for store integration (appending conditions) |
|
|
|
|
|
|
|
|
|
|
|
|
## SQL Generation Notes
|
|
|
|
|
|
|
|
|
|
|
|
- **Placeholders** — `?` is used for SQLite/MySQL, `$n` for Postgres. The renderer
|
|
|
|
|
|
tracks offsets to compose queries with pre-existing arguments.
|
|
|
|
|
|
- **JSON Fields** — Memo metadata lives in `memo.payload`. The renderer handles
|
|
|
|
|
|
`JSON_EXTRACT`/`json_extract`/`->`/`->>` variations and boolean coercion.
|
|
|
|
|
|
- **Tag Operations** — `tag in [...]` and `"tag" in tags` become JSON array
|
|
|
|
|
|
predicates. SQLite uses `LIKE` patterns, MySQL uses `JSON_CONTAINS`, and
|
|
|
|
|
|
Postgres uses `@>`.
|
|
|
|
|
|
- **Boolean Flags** — Fields such as `has_task_list` render as `IS TRUE` equality
|
|
|
|
|
|
checks, or comparisons against `CAST('true' AS JSON)` depending on the dialect.
|
|
|
|
|
|
|
|
|
|
|
|
## Typical Integration
|
|
|
|
|
|
|
|
|
|
|
|
1. Fetch the engine with `filter.DefaultEngine()`.
|
|
|
|
|
|
2. Call `CompileToStatement` using the appropriate dialect enum.
|
|
|
|
|
|
3. Append the emitted SQL fragment/args to the existing `WHERE` clause.
|
|
|
|
|
|
4. Execute the resulting query through the store driver.
|
|
|
|
|
|
|
|
|
|
|
|
The `helpers.AppendConditions` helper encapsulates steps 2–3 when a driver needs
|
|
|
|
|
|
to process an array of filters.
|