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.
Timestamp accessors previously compiled only on schema fields, so a saved
shortcut like created_ts.getMonth() == now.getMonth() && created_ts.getDate()
== now.getDate() ("on this day") failed with unknown identifier "now" and the
only workaround froze literal month/day values into the filter.
Accessors on `now` now fold to literal date parts of the frozen per-compile
clock (UTC, CEL bases: 0-based month/day-of-week), so such filters re-resolve
on every query. Comparisons are also normalized for the renderer: a folded
literal on the left swaps operands with the operator mirrored, and
literal-vs-literal comparisons fold to a constant condition.
|
4 days ago | |
|---|---|---|
| .. | ||
| MAINTENANCE.md | ||
| README.md | 4 days ago | |
| engine.go | 3 weeks ago | |
| engine_test.go | ||
| functions_test.go | 4 days ago | |
| helpers.go | ||
| ir.go | 3 weeks ago | |
| parser.go | 4 days ago | |
| render.go | 3 weeks ago | |
| schema.go | 3 weeks ago | |
| time_test.go | 3 weeks ago | |
README.md
Memo Filter Engine
This package houses the memo-only filter engine that turns standard CEL syntax into SQL fragments for the subset of expressions supported by the memo schema. 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, and non-standard legacy coercions are rejected. - 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. - Time Fields —
created_ts,updated_ts, and attachmentcreate_timeare CELtimestampvalues. Express instants with thenowvariable,duration("…")(e.g.created_ts >= now - duration("24h")), ortimestamp("2006-01-02T15:04:05Z")/timestamp(<epoch-seconds>). These fold to epoch seconds at compile time —nowis frozen once per compile (injectable for tests via the engine clock) — so the backing columns stay unchanged. - 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. - String Matching —
content.contains(x),content.startsWith(x), andcontent.endsWith(x)render as case-insensitiveLIKE/ILIKEwith LIKE metacharacters (%,_,\) escaped. Available on scalar string fields whose schema setsSupportsContains(memocontent; attachmentfilename,mime_type). - Regex —
field.matches("pattern")renders to~(Postgres) orREGEXP(MySQL/SQLite). SQLite uses a Go-backedregexpfunction registered instore/db/sqlite/functions.go. Patterns are validated at compile time against Go's RE2 viacel.ValidateRegexLiterals(). Caveat: regex syntax differs per engine (Go RE2 on SQLite, POSIX ERE on Postgres, ICU on MySQL 8.0+), so engine-specific patterns may not be portable. - Tag
all()/exists_one()—tags.all(t, <pred>)matches only non-empty tag sets where every element satisfies the predicate;tags.exists_one(t, <pred>)matches when exactly one element does (COUNT(...) = 1). Both iterate per-element (json_each/jsonb_array_elements_text/JSON_TABLE). - Timestamp Accessors —
created_ts.getFullYear(),getMonth(),getDate(),getDayOfMonth(),getDayOfWeek(),getDayOfYear(),getHours(),getMinutes(),getSeconds()render to date-part extraction (strftime/EXTRACT/YEAR/MONTH/…). Results are normalized to CEL's base (0-based month, 0-based day-of-week with 0 = Sunday). The same accessors onnowfold to literal date parts of the frozen evaluation time (UTC), so saved filters likecreated_ts.getMonth() == now.getMonth() && created_ts.getDate() == now.getDate()("on this day") re-resolve on every compile. Extraction is UTC on SQLite/Postgres (epoch columns); on MySQL theTIMESTAMPcolumn is read in the session time zone. A timezone argument is not supported. - Set Operations —
ext.Sets():sets.contains(tags, [...]),sets.intersects(tags, [...]), andsets.equivalent(tags, [...])desugar to exact-membership checks (AND / OR of"v" in tags);equivalentadds asize(tags)length check (relies on tags being a set). size()—size(tags)renders to JSON array length;size(content)(and other string fields) render toLENGTH/CHAR_LENGTH(MySQL) for code-point counts.- Arithmetic —
+,-,*,/,%constant-fold on literal/now/durationoperands (division and modulo guard against a zero divisor).
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.