Repository navigation
Description
Goal
Audit every OpenOptions field. Decide which ones should also be overridable per query (or per transaction or per cursor), and record which ones already are. The result should be a table in the docs plus follow-up issues for any gaps worth closing.
Current state (v3, as of 2026-11-05)
OpenOptions field |
Per-query override today | Notes |
|---|---|---|
max_derived_facts |
Yes: :max-derived-facts N |
Replaces the database value for that query (#553) |
max_results (deprecated alias) |
Yes: :max-results N |
The lower of the two applies (#553) |
query_memory_limit (QRY-012) |
Check | Confirm whether any per-query, prepared or cursor path can set it |
| (round limit, not an option yet) | No | Hard-coded 1000; see #861 |
transaction_chunk_facts |
Check | Per-transaction rather than per-query; does begin_write take it? |
synchronous |
Check | Per-transaction durability (e.g. a bulk import with SyncMode::Normal) may be useful |
wal_checkpoint_threshold |
No | Database-wide policy; probably stays open-only |
page_cache_size |
No | Shared cache; probably stays open-only |
allow_unlocked |
No | Open-time only by nature |
read_only |
No | Open-time only by nature |
Prepared queries
PreparedQuery copies the database's max_derived_facts and query_memory_limit when it is prepared (src/query/datalog/prepared.rs). Its only per-statement override is a literal :max-derived-facts N in the prepared text. What's missing:
- No bind slots. The limit keywords take only literals (
:max-derived-facts $nisINT-015), unlike:limit $n/:offset $n(No working result-limiting/pagination mechanism for plain (non-recursive) :find queries #306). - No per-statement setter on
PreparedQueryor in the bindingPreparedQueryAPIs (post-1.0: expose PreparedQuery over UniFFI #181). - No way to override
query_memory_limitfor one prepared statement.
For every option found to be per-query overridable, the audit must also decide the prepared-query form: a literal in the text, a bind slot, a PreparedQuery setter, or a mix. Each form must be exposed in all bindings.
Proposed safety mechanism: OpenOptions::allow_overrides
The embedder controls which settings a query may override. This addresses the safety question raised in question 2 below.
- The field:
OpenOptions::allow_overrides: Vec<Overridable>(allowOverrides: []in the bindings), whereOverridableis a#[non_exhaustive]enum with one variant per overridable setting:MaxDerivedFacts,MaxResults,MaxIterations(Make the recursion round limit (max_iterations) configurable per query and in OpenOptions #861),QueryMemoryLimit, and whatever else this audit adds. - Default (decided 2026-11-06): the field holds a default list of variants, used when the embedder doesn't set it.
- When the embedder sets it, only the listed variants are allowed: the given list replaces the default and is not merged with it. An empty list allows no overrides at all.
- Adding one setting therefore means restating the defaults. Accepted: every alternative (merge semantics, separate allow/deny lists) is at least as tricky. To soften this, publish the default list as a constant, e.g.
Overridable::DEFAULTS, so a caller can write[Overridable::DEFAULTS, &[Overridable::QueryMemoryLimit]].concat(), with an equivalent in each binding.
- Contents of the default list are decided per option by this audit. Proposal: it holds
MaxDerivedFactsandMaxResults, which queries can override today, so existing queries keep working.QueryMemoryLimitstays out of it, so query text can't change the memory cap unless the embedder opts in. - Override not allowed: an override of a setting not in the list, whether literal or bind slot, is refused with a new error code (API- or QRY-, registered in
docs/ERROR_REFERENCE.md), never silently ignored or clamped. - Scope (decided 2026-11-06): the list governs query text only: keywords and their bind slots, including in prepared query text. Settings applied from application code (e.g. a
PreparedQuerysetter or a binding equivalent) aren't gated, because application code is trusted. - Override direction (deferred, 2026-11-06): whether lowering a limit should be allowed for unlisted settings is not part of this work. Revisit only if needed.
Questions to answer per option
- Is a per-query, per-transaction or per-cursor override meaningful, or is the setting inherently database-wide (cache, locking, WAL policy)?
- If it is meaningful, may the override raise the value above the database's or only lower it? Example: should a single query be able to exceed the
query_memory_limitthe embedder chose? This is a safety question when query text comes from an untrusted or LLM-generated source. - Where does the override live: query keyword (
:max-…), bind slot,PreparedQuerysetter, cursor API,begin_writeoptions, or the binding APIs? - How do prepared queries and bind slots interact with it (see v2.x: prepared queries ignore OpenOptions max_derived_facts / max_results #679)?
Deliverables
- A table of every option in
docs/and on the docs site (performance-tuning.mdand/ordatalog-reference.md): open-only or overridable, at which level, and raise or lower semantics. - One follow-up issue per gap worth closing, each with a philosophy check (zero-configuration: defaults must stay right without any override).
- Bindings: list where an existing override isn't exposed.
Activity
Merge pull request #868 from project-minigraf/docs/862-override-audit
adityamukho commentedon Nov 6, 2026
Spec merged in #868. Implementation split into sub-issues: #869 (gate, unblocks #861), #870 (:query-memory-limit, slots, setters), #871 (TxOptions), #872 (bindings). #862 closes when all four are done.