8000 fix(prepared): prepared queries use the database's rule limits (#679) by adityamukho · Pull Request #682 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **Prepared queries use the database's rule limits** (#679), backported from v3.0.0. `PreparedQuery::execute` ran a rule query with the default `max_derived_facts` and `max_results` of 1,000,000 whatever `OpenOptions` set, so a lower limit set to stop a runaway recursive rule did not apply to prepared queries, and a higher one set for a large derivation failed with `INT-020` where `execute` of the same query succeeded. It now uses the database's limits, as `execute` does. `:max-derived-facts` and `:max-results` written in the query text were always applied. Present since v0.19.0.

## v2.0.9 — 2026-10-21

Patch release on the v2.x line under the [support policy](PHILOSOPHY.md#support-policy): two data-integrity fixes, for a pseudo-attribute in a rule body that read the wrong valid time and tx metadata (#661) and for a fact of more than four elements whose extra elements were silently dropped (#649), plus error codes in `BrowserDb` rejections (#673). File format is unchanged (v7). No API changes; a fact of more than four elements, which used to be stored truncated, now fails with `PRS-108`. Upgrading is recommended for every v2.x user who reads `:db/valid-from`, `:db/valid-to`, `:db/tx-count` or `:db/tx-id` in rules.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ Auto-migrates v1/v2/v3/v4/v5/v6 → v7 on open/checkpoint.

## Test Coverage

**1318 tests** (1309 passing, 9 ignored; unit + integration + doc).
**1321 tests** (1312 passing, 9 ignored; unit + integration + doc).
See `docs/TEST_COVERAGE.md` for the full per-file breakdown.

**Testing conventions** — see the Testing Conventions section below before writing any tests.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ let r2 = pq.execute(&[("tx", BindValue::TxCount(2)), ("entity", BindValue::Entit

```bash
cargo run # interactive Datalog REPL
cargo test # run 1318 tests
cargo test # run 1321 tests
cargo run < demos/demo_recursive.txt # recursive rules demo
```

Expand Down
2 changes: 1 addition & 1 deletion docs/TEST_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ For v2.0.5, torn-WAL-tail tests (#513) check that opening a WAL writer cuts off

`tests/crash_kill_test.rs` kills a child process running a seeded workload under five checkpoint modes at a random point and checks that the reopened file holds exactly the transactions the child logged as committed, or those plus the one in flight, through full-scan, AEVT, AVET, EAVT and `:as-of` queries, again after a second reopen and after a checkpoint plus reopen (#384). The workload leaves out the v2.x known issues #371 and #435.

`tests/prepared_statements_test.rs` checks that a prepared query with a literal `:as-of`, `:valid-at` or `:any-valid-time` returns the same rows as the unprepared query (#603).
`tests/prepared_statements_test.rs` checks that a prepared query with a literal `:as-of`, `:valid-at` or `:any-valid-time` returns the same rows as the unprepared query (#603), and that a prepared rule query fails with `INT-020` one below the database's `max_derived_facts` or `max_results` and succeeds at it, as `execute` does (#679).

`tests/magic_sets_test.rs` checks rule queries with a literal argument where some call site cannot carry the magic-sets demand (#608): a rule-body call with an argument nothing binds yet, the same rule called with free arguments, a call from a rule called with free arguments, calls inside `or` and `not`, a fully literal call, and a call bound only by a variable; `magic_sets.rs` unit tests check the adornment each shape solves to.

Expand Down
4 changes: 4 additions & 0 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,10 @@ impl Minigraf {
self.inner.fact_storage.clone(),
self.inner.rules.clone(),
self.inner.functions.clone(),
(
self.inner.options.max_derived_facts,
self.inner.options.max_results,
),
)
}

Expand Down
7 changes: 6 additions & 1 deletion src/query/datalog/prepared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ pub struct PreparedQuery {
fact_storage: FactStorage,
rules: Arc<RwLock<RuleRegistry>>,
functions: Arc<RwLock<FunctionRegistry>>,
/// The database's `max_derived_facts` and `max_results`.
limits: (usize, usize),
}

impl std::fmt::Debug for PreparedQuery {
Expand Down Expand Up @@ -115,11 +117,12 @@ impl PreparedQuery {

let filled_query = substitute(&self.template, &binding_map)?;

let executor = DatalogExecutor::new_with_rules_and_functions(
let mut executor = DatalogExecutor::new_with_rules_and_functions(
self.fact_storage.clone(),
self.rules.clone(),
self.functions.clone(),
);
executor.set_limits(self.limits.0, self.limits.1);
executor.execute(DatalogCommand::Query(filled_query))
}
}
Expand All @@ -131,6 +134,7 @@ pub(crate) fn prepare_query(
fact_storage: FactStorage,
rules: Arc<RwLock<RuleRegistry>>,
functions: Arc<RwLock<FunctionRegistry>>,
limits: (usize, usize),
) -> Result<PreparedQuery> {
validate_no_attribute_slots(&query)?;
let slot_names = collect_slot_names(&query);
Expand All @@ -140,6 +144,7 @@ pub(crate) fn prepare_query(
fact_storage,
rules,
functions,
limits,
})
}

Expand Down
51 changes: 51 additions & 0 deletions tests/prepared_statements_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,3 +499,54 @@ fn prepared_literal_as_of_and_valid_at_are_kept() {
2
);
}

// ─── Database limits (#679) ───────────────────────────────────────────────────

/// A friend chain a -> b -> c -> d -> e with a recursive `reach` rule:
/// 10 derived facts in all, and the last pass derives all 10 again.
fn reach_db(opts: minigraf::OpenOptions) -> Minigraf {
let db = opts.open_memory().unwrap();
db.execute("(transact [[:a :friend :b] [:b :friend :c] [:c :friend :d] [:d :friend :e]])")
.unwrap();
db.execute("(rule [(reach ?x ?y) [?x :friend ?y]])")
.unwrap();
db.execute("(rule [(reach ?x ?z) (reach ?x ?y) [?y :friend ?z]])")
.unwrap();
db
}

fn execute_reach(db: &Minigraf) -> Result<QueryResult, minigraf::MinigrafError> {
db.execute("(query [:find ?x ?y :where (reach ?x ?y) [?y :friend :e]])")
}

fn prepared_reach(db: &Minigraf) -> Result<QueryResult, minigraf::MinigrafError> {
db.prepare("(query [:find ?x ?y :where (reach ?x ?y) [?y :friend $z]])")
.unwrap()
.execute(&[("z", BindValue::Val(Value::Keyword(":e".to_string())))])
}

#[test]
fn prepared_rule_queries_use_the_database_max_derived_facts() {
let db = reach_db(minigraf::OpenOptions::new().max_derived_facts(9));
assert_eq!(execute_reach(&db).expect_err("10 > 9").code(), "INT-020");
assert_eq!(prepared_reach(&db).expect_err("10 > 9").code(), "INT-020");
}

#[test]
fn prepared_rule_queries_use_the_database_max_results() {
let db = reach_db(minigraf::OpenOptions::new().max_results(9));
assert_eq!(execute_reach(&db).expect_err("10 > 9").code(), "INT-020");
assert_eq!(prepared_reach(&db).expect_err("10 > 9").code(), "INT-020");
}

#[test]
fn prepared_rule_queries_succeed_at_the_database_limits() {
// Exactly at both limits.
let db = reach_db(
51F4 minigraf::OpenOptions::new()
.max_derived_facts(10)
.max_results(10),
);
execute_reach(&db).unwrap();
prepared_reach(&db).unwrap();
}
Loading
0