MindQL is a small declarative query language for the engrava thought-graph. It provides a human-readable syntax for retrieving and counting rows, plus a read-only SQL passthrough.
FIND <table> [WHERE <condition> [AND <condition> ...]] [LIMIT <n>]
COUNT <table> [WHERE <condition> [AND <condition> ...]]
SELECT <raw read-only SQL>
- The command verb (
FIND,COUNT,SELECT) is case-insensitive. FINDandCOUNTrequire a table name as the second token.- A
WHEREclause isfield operator value; string values must be single-quoted, bare numbers are coerced toint/float. The quoting decides the type: a single-quoted value is kept verbatim as a string, so a zero-padded identifier likesource = '007'matches the stored string'007', whereas an unquotedcreated_cycle = 7is coerced to the integer7. - Operators:
=,!=,>,<,>=,<=. Conditions chain withAND.
| Token(s) | Table |
|---|---|
thoughts, thought |
thought |
edges, edge |
edge |
embeddings, embedding |
embedding |
actions, action |
action |
Retrieve rows from a table.
FIND thoughts WHERE thought_type = 'OBSERVATION' LIMIT 10
FIND thoughts WHERE lifecycle_status = 'ACTIVE' AND priority = 'P1'
FIND edges WHERE edge_type = 'ASSOCIATED' LIMIT 5
Filterable thought columns include thought_type, lifecycle_status,
priority, essence, content, source, confidence, visibility,
confirmation_count, created_cycle, updated_cycle, and thought_id.
A column outside the per-table allowlist raises MindQLParseError.
Default row cap. A FIND with no LIMIT clause is capped at 100 rows
when it runs, so an unqualified FIND thoughts can never trigger an unbounded
scan. The cap is applied at execution, not at parse time — parse("FIND thoughts")
leaves query.limit as None, and the executor substitutes the default only if
no explicit LIMIT is present. An explicit LIMIT always overrides the default;
COUNT queries are unaffected (they aggregate and never materialise the rows).
Returns: matching rows as dicts.
Count rows matching the filters. COUNT does not accept a LIMIT.
COUNT thoughts WHERE lifecycle_status = 'ACTIVE'
COUNT thoughts WHERE thought_type = 'OBSERVATION'
COUNT edges
Returns: the count is exposed on MindQLResult.count.
SELECT is a read-only SQL passthrough — it runs the statement verbatim,
so it needs a full FROM clause and standard SQL syntax. The underlying
tables are thought, edge, embedding, and action.
SELECT thought_id, essence FROM thought WHERE lifecycle_status = 'ACTIVE'
SELECT thought_id, priority, essence FROM thought WHERE thought_type = 'BELIEF' LIMIT 20
Only statements that begin with SELECT are permitted; anything else is
rejected.
FIND and COUNT against the thoughts and edges tables accept four opt-in
valid-time predicates in the WHERE clause, for querying when a fact was
true in the world (the second time axis — see
The Bi-temporal Model for the full semantics):
FIND thoughts WHERE valid_now
FIND edges WHERE valid_at '2026-01-01T00:00:00+00:00'
FIND thoughts WHERE priority = 'P1' AND valid_within '2026-01-01T00:00:00+00:00' '2026-02-01T00:00:00+00:00'
FIND thoughts WHERE valid_between '2026-01-01T00:00:00+00:00' '2026-12-31T00:00:00+00:00'
valid_nowtakes no argument;valid_attakes one ISO-8601 timestamp;valid_withinandvalid_betweentake two.- They combine with ordinary conditions via
AND. valid_now/valid_at/valid_withinare NULL-tolerant (a record with an openvalid_from/valid_untilbound stays in the result);valid_betweenrequires real bounds on both ends and therefore excludes open-bound rows.- A query that uses no temporal predicate behaves exactly as before.
Valid time is predicate-only, not a filterable column. Query valid time only through the four predicates above.
valid_fromandvalid_untilare not in the per-table column allowlist, so an ordinary comparison such asWHERE valid_from = '2026-01-01T00:00:00+00:00'is rejected when the query runs (MindQLParseError: Column 'valid_from' not allowed for table 'thought') — usevalid_at/valid_within/valid_betweeninstead.
The semantics, the open-interval (NULL = ±∞) rule, and invalidate are
documented in full on The Bi-temporal Model.
Custom MindQL verbs are provided through an extension's
ExtensionManifest.mindql_extensions and reach the executor via the
extensions= argument (entry-point discovery wires this up automatically).
See Extensions for the registration flow.
parse() returns a MindQLQuery plan. Its fields are command, table,
conditions, temporal_predicates (the parsed valid-time predicates, empty when
none are used), limit, raw_sql, extension_name, and extension_args.
from engrava import parse, MindQLParseError
try:
query = parse("FIND thoughts WHERE thought_type = 'OBSERVATION' LIMIT 5")
print(query.command) # MindQLCommand.FIND
print(query.table) # "thought"
print(query.conditions) # [Condition(field='thought_type', operator=..., value='OBSERVATION')]
print(query.limit) # 5
except MindQLParseError as exc:
print(f"Parse error: {exc}")MindQLExecutor runs against an open aiosqlite.Connection, and execute()
takes a parsed MindQLQuery — parse the string first. MindQLResult
exposes columns, rows, count, and command.
from engrava import MindQLExecutor, parse
executor = MindQLExecutor(conn) # conn is an aiosqlite.Connection
result = await executor.execute(
parse("FIND thoughts WHERE thought_type = 'OBSERVATION' LIMIT 10")
)
for row in result.rows:
print(row["essence"])
count_result = await executor.execute(
parse("COUNT thoughts WHERE lifecycle_status = 'ACTIVE'")
)
print(f"Active thoughts: {count_result.count}")MindQLParseError— raised for syntax errors, unknown tables, or columns outside a table's allowlist.- Unknown command verbs raise
MindQLParseErrorunless registered as an extension command. - A
WHEREfragment must match thefield operator valuegrammar in full. Trailing content after a condition (for exampleWHERE priority = 'P1' OR 1=1) is rejected with aMindQLParseErrorrather than silently parsing only the leadingpriority = 'P1'and discarding the rest — so a malformed condition can never quietly change the result set.
# Find observations
engrava --db my.db query "FIND thoughts WHERE thought_type = 'OBSERVATION' LIMIT 5"
# Count active thoughts
engrava --db my.db query "COUNT thoughts WHERE lifecycle_status = 'ACTIVE'"
# Select specific fields (raw SQL passthrough)
engrava --db my.db query "SELECT thought_id, essence FROM thought LIMIT 5"Output formats: --format table (default), --format json, --format csv.