mirror of https://github.com/usememos/memos
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Add support for CEL exists() comprehension with startsWith, endsWith, and
contains predicates to enable powerful tag filtering patterns.
Features:
- tags.exists(t, t.startsWith("prefix")) - Match tags by prefix
- tags.exists(t, t.endsWith("suffix")) - Match tags by suffix
- tags.exists(t, t.contains("substring")) - Match tags by substring
- Negation: !tags.exists(...) to exclude matching tags
- Works with all operators (AND, OR, NOT) and other filters
Implementation:
- Added ListComprehensionCondition IR type for comprehension expressions
- Parser detects exists() macro and extracts predicates
- Renderer generates optimized SQL for SQLite, MySQL, PostgreSQL
- Proper NULL/empty array handling across all database dialects
- Helper functions reduce code duplication
Design decisions:
- Only exists() supported (all() rejected at parse time with clear error)
- Only simple predicates (matches() excluded to avoid regex complexity)
- Fail-fast validation with helpful error messages
Tests:
- Comprehensive test suite covering all predicates and edge cases
- Tests for NULL/empty arrays, combined filters, negation
- Real-world use case test for Issue #5480 (archive workflow)
- All tests pass on SQLite, MySQL, PostgreSQL
Closes #5480
|
5 months ago | |
|---|---|---|
| .. | ||
| MAINTENANCE.md | 8 months ago | |
| README.md | 8 months ago | |
| engine.go | 6 months ago | |
| helpers.go | 8 months ago | |
| ir.go | 5 months ago | |
| parser.go | 5 months ago | |
| render.go | 5 months ago | |
| schema.go | 5 months ago | |
README.md
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:
- Parsing – CEL expressions are parsed with
cel-goand validated against the memo-specific environment declared inschema.go. Only fields that exist in the schema can surface in the filter. - 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). - Rendering – the renderer in
render.gowalks the IR and produces a SQL fragment plus placeholder arguments tailored to a target dialect (sqlite,mysql, orpostgres). Dialect differences such as JSON access, boolean semantics, placeholders, andLIKEvsILIKEare 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:
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,$nfor Postgres. The renderer tracks offsets to compose queries with pre-existing arguments. - JSON Fields — Memo metadata lives in
memo.payload. The renderer handlesJSON_EXTRACT/json_extract/->/->>variations and boolean coercion. - Tag Operations —
tag in [...]and"tag" in tagsbecome JSON array predicates. SQLite usesLIKEpatterns, MySQL usesJSON_CONTAINS, and Postgres uses@>. - Boolean Flags — Fields such as
has_task_listrender asIS TRUEequality checks, or comparisons againstCAST('true' AS JSON)depending on the dialect.
Typical Integration
- Fetch the engine with
filter.DefaultEngine(). - Call
CompileToStatementusing the appropriate dialect enum. - Append the emitted SQL fragment/args to the existing
WHEREclause. - 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.