Land #1528 (Iceberg audit + browse-parity + family-C) onto main - #1554
Open
aaj3f wants to merge 45 commits into
Open
Land #1528 (Iceberg audit + browse-parity + family-C) onto main#1554aaj3f wants to merge 45 commits into
aaj3f wants to merge 45 commits into
Conversation
Delivers the #1508 duplicate-triples / assert-retract-resurrection fix (fluree-db-indexer dedup_fact_lifecycles) to the virtual-dataset perf lineage — audit finding F-AUD-2 (audit-2026-07/00-MASTER-AUDIT.md). Merge verified conflict-free pre-flight via git merge-tree (2 files auto-merged in disjoint regions; decision record audit-2026-07/decisions/DEC-001-pr-bundling.md, Adjudication B).
Iceberg merge-on-read (position/equality) delete files are recognized but never applied on the virtual read path: delete manifests are dropped and only the live data files are read. A query over a MoR-maintained table silently returns deleted rows as if live, and the manifest COUNT/row-total sums over-count by the delete cardinality — a silent wrong answer, the worst failure class for a database. Copy-on-write deletes are unaffected (status=DELETED entries are already dropped). Until delete application lands, fail closed: refuse any Iceberg read whose snapshot carries delete files. Two independent signals are checked so a snapshot cannot slip through — the snapshot summary counters (zero extra I/O) and the manifest list (backstop for summaries that omit/under-count). The refusal gates both scan planners (runtime-agnostic + Send) and the stats/COUNT preview path, and is disabled by FLUREE_ICEBERG_ALLOW_MOR_DELETES (default off; warns once per table and restores the prior skip-and-proceed behavior). Adds Snapshot::total_delete_files / total_position_deletes / total_equality_deletes accessors, an IcebergError::MergeOnReadDeletes variant, real content=1 delete fixtures driving the planner + stats guards through to a refusal, and switch docs. Closes audit finding F-AUD-1.
The fail-closed merge-on-read guard lives in plan_scan, but scan_table_inner's two scan-files cache-HIT arms (in-memory + persistent disk) rebuild scan tasks directly from a cached file list WITHOUT calling plan_scan — so a delete-bearing entry could bypass the guard. The disk cache is the real exposure: a scan-files entry written by a pre-guard binary for a merge-on-read snapshot survives a restart and would be served silently by a guarded binary. Close both bypasses: - ScanPlan and CachedScanFiles / PersistedScanFiles gain has_delete_manifests, stamped by the planner (summary counters OR a content=1 delete manifest). It is only ever true under the override, since the guard refuses otherwise. - Both hit arms re-check it via a shared guard_cached_scan_files helper (same MergeOnReadDeletes refusal, override-aware) so the check cannot drift. - Bump CACHE_FORMAT_VERSION 2 -> 3: every pre-guard (v2) scanfiles entry is now a clean miss and is recomputed THROUGH the guard rather than served. Tests: disk round-trip of the flag; a downgraded-to-v2 scanfiles entry is a miss (recompute); the shared hit-arm helper refuses a delete-bearing entry naming the override switch. Gates: fmt clean; clippy -p fluree-db-iceberg -D warnings clean; clippy -p fluree-db-api (iceberg) clean; iceberg 240 + api 798 tests pass. Audit F-AUD-1 cache-arm follow-up (extends the #1520 guard onto the perf line).
…imate + per-query division F-AUD-3 sites D and C (audit-2026-07/V2-membudget-verification.md §6, §3). D: BINDING_EST_BYTES was a hand-picked 64 — a 27% under-count of the true 88-byte size_of::<Binding>() (binding.rs:14-17), so every accounted operator checkpointed late. Derive it from the type and add a compile-time `const _` guard that refuses any future re-pin below the stack size. It is still a floor (ignores the Arc<str> IRI heap a wide crawl carries, ~2.2x) — documented on the constant. C: set_memory_limit had no production caller, so N concurrent queries each compared their own counter against the FULL process budget (two 5 GB queries both read "under 8 GB" while the node sits at 10 GB). Pin a per-query ceiling of budget / FLUREE_QUERY_BUDGET_SHARE_DIV at the runner attach point. Default div=1 pins nothing — byte-for-byte today's behavior; an embedder's explicit ceiling is never clobbered. The sound dynamic form (divide by ACTUAL live top-level concurrency) needs the server request boundary (query_control.rs) to avoid miscounting nested policy/reasoning/sub-queries, and is deferred there. Tests: binding_est_bytes_is_at_least_binding_stack_size, per_query_ceiling_divides_and_floors, shared_ceiling_trips_each_query_at_its_divided_budget.
F-AUD-3 sites A1 and A2 (audit-2026-07/V2-membudget-verification.md §1, §4). The non-aggregate scan path had zero record_alloc / checkpoint — six check_cancelled only — so a wide crawl was invisible to the R3-B memory budget and OOM'd instead of aborting typed. Specimen 071cd59f (a point-lookup crawl that hard-OOM'd at 10237 MB) lived exactly here. A1: record each materialized window (produced_rows * cols * BINDING_EST) in advance_one_window and upgrade the pull-loop poll from check_cancelled() to checkpoint(), so cumulative window bytes trip a typed MemoryBudgetExceeded (507) before the loop pulls another window. One window is bounded (~materialize_window_rows) so it cannot itself OOM. A2: the fact-as-parent build (build_parent_lookup) transiently materializes a full parent-sized map (tens of millions of entries) unbounded by the memo cap — the cap only refuses to RETAIN it after it is fully built. Thread ctx in, account each batch, and checkpoint inside the build loop so it aborts typed BEFORE the whole map is resident. Both gated by FLUREE_SCAN_MEM_ACCOUNTING (default on; off is a clean revert — the scan records nothing, so checkpoint degrades to a pure cancellation poll). The counter is query-lifetime cumulative (no decrement), conservative for a streaming scan, matching the existing fold/join accounting. Per-file buffer accounting (V2 site B) is excluded — it needs a decrement primitive the monotonic counter lacks. Hermetics: r3b_scan_window_budget_aborts_typed (the 071cd59f regression), r3b_parent_build_budget_aborts_typed.
…e-abort)
The live re-bless caught a regression from the A1 scan-window accounting: q038 (a
36M-row un-fused COUNT on the per-row materialize path) false-aborted typed at 38s
("Query memory budget exceeded: ~8.61 GB > 8.59 GB") while completing fine at 52.5s
with FLUREE_SCAN_MEM_ACCOUNTING=off and bounded resident memory. Root cause is the
documented cumulative-no-decrement edge (V2): ~70 sequentially-FREED 512K-row scan
windows SUM past the budget even though only one window is ever resident — a
false-positive typed abort on exactly the long-scan class the accounting protects.
Fix — window-scoped release:
- add QueryCancellation::release (fluree-db-core) + the ExecutionContext::release
wrapper: a saturating decrement of the budget counter, valid ONLY for allocations
with a provable drop point. Documented caller invariant: never release a
persistent allocation (the guard would then under-count live memory).
- pair each A1 window charge in advance_one_window with a release once the window
is emitted/handed off (`produced` drops), so a streaming scan accounts only its
resident window, not the all-time sum. Charge + checkpoint still happen BEFORE
emit, so an oversized single window — or this window atop a retained A2 build or
an upstream fold — still aborts typed.
- A2 fact-parent build charge stays cumulative (that map genuinely persists);
fold/join/fused accounting untouched (their buffers persist too).
Regression test r3b_scan_windows_release_no_false_abort: 64 one-row windows under an
8000-byte ceiling COMPLETE (verified to fail pre-fix at window ~16 with
MemoryBudgetExceeded 8448 > 8000). The single-window abort (071cd59f) and
parent-build abort tests still pass. New core tests cover release saturating-sub +
disabled-handle no-op.
…it item 7, F-AUD-5] The pruning backend already evaluates `Expression::In` (both manifest-level `can_contain_file` and row-group `row_group_can_contain`); nothing emitted it. Add a `ScanCmpOp::In` + `ScanValue::Set` arm to the pushdown vocabulary and emit it from (a) `FILTER(?x IN (c1..cN))` and (b) single-var `VALUES` blocks feeding a scalar-column POM (the exactly-one-scalar-POM soundness gate the existing pushdown uses). Whole-or-nothing: a member that can't push declines the entire IN (a partial IN could prune a file a dropped member needs). Bounded by `FLUREE_R2RML_IN_PUSHDOWN_MAX` (=64). An emitted `In` drives file + row-group pruning; it is transparent at the Arrow row filter (a sibling comparison still builds its filter) and the in-engine FILTER/VALUES join stays authoritative. Kill switch: `FLUREE_R2RML_IN_PUSHDOWN` (default on). This commit also introduces the shared scan-side pushdown-vocabulary changes (the `ScanValue` enum arms and the `build_iceberg_filter` -> `scan_value_to_literal` refactor) that items 8/10 build on. Corpus: q069 (FILTER-IN FK-ref parity; scalar prune shown by q070), q070 (scalar VALUES prune-to-zero). FK-IRI IN pruning is a documented follow-on.
…em 8, F-AUD-6] Scan-side top-k was DESC-only: SPARQL orders unbound (NULL) values FIRST under ASC, so a nullable column could hide an unread top-k row. Admit ASC ONLY when the sort column is REQUIRED (non-nullable per the Iceberg schema) — then no NULL rows exist and the ASC mirror (read files by `lower_bound` ascending, stop when the next `lower_bound` strictly exceeds the k-th bound) is sound. The `TopKBound` engine is generalized worst-first (the DESC regression tests are preserved and pass); the direction threads `set_topk(var, k, ascending)` -> `ScanTopK.ascending` -> `TopKConfig`, and the provider re-checks `field.required` before honoring an ASC directive (declining just falls through to the full parallel scan — the authoritative SortOperator applies the exact order + LIMIT). DISTINCT-kill untouched. Kill switch: `FLUREE_R2RML_TOPK_ASC` (default on; OFF is byte-identical to the pre-item-8 DESC-only behavior). Corpus: q071 (ASC ORDER BY LIMIT with a unique tiebreaker -> deterministic, Full-hash).
…g [audit item 10, F-AUD-11]
`ScanValue` had no timestamp arm, so dateTime predicates never reached pruning
even though the Iceberg manifest value-codec decodes timestamp/timestamptz bounds
unambiguously. Add `ScanValue::Timestamp { micros, tz }` (emitted from a
`FILTER` on an xsd:dateTime column; `tz` = whether the literal is timezone-aware)
and a matching `LiteralValue::TimestampTz`. The push is FRAME-MATCHED at build
time — a UTC (tz-aware) literal only against a `timestamptz` column, a naive
(wall-clock) literal only against a `timestamp` column — so the micros are
directly comparable to the file's decoded bounds; any mismatch declines (the
in-engine FILTER enforces). Pruning is MANIFEST-LEVEL ONLY: no row-group
`stat_bounds` timestamp arm is added, because a Parquet INT64 timestamp's logical
unit (milli/micro/nano) is ambiguous from row-group stats alone (A1 §4) — the
`stat_bounds` catch-all keeps every row group for a timestamp predicate.
Kill switch: `FLUREE_ICEBERG_TIMESTAMP_STATS` (default on, mirroring
NUMERIC_STATS). Corpus: q072 (EVENT_TS range on FACT_WEB_EVENT). Prunes when
EVENT_TS is `timestamptz`; if it is `timestamp` (ntz), the frame gate declines and
the FILTER still enforces — correct either way.
…ervable [audit item 11, F-AUD-7]
The engine-wide `set_row_budget` default was a silent no-op. OPTIONAL (left-outer
join) now forwards the budget to its REQUIRED (outer) side — sound because each
required row yields >=1 output row (matched or null-padded) and required order is
preserved, so bounding the outer side to k still yields >=k output for the LIMIT
to truncate; the inner/optional side is deliberately NOT budgeted. This closes
probe-04's measured 68,828x read amplification.
For FILTER/SORT/DISTINCT/HAVING/AGGREGATE/GROUP-AGGREGATE/SUBQUERY the swallow was
replaced with an EXPLICIT documented decline (a debug span) so it is observable in
traces. MINUS also declines: DEVIATION from the item brief ("MINUS forwards to
primary") on soundness grounds — an anti-join's output is a subset of its primary
with multiplicity <=1, so a finite primary budget can under-produce (the identical
reasoning the inner nested-loop join uses to absorb rather than forward,
join.rs:1127).
Kill switch: `FLUREE_R2RML_BUDGET_OPTIONAL` (default on; OFF swallows the budget,
byte-identical results). Corpus: q073 (OPTIONAL budget) + q074 (its plain-LIMIT
control).
… [audit item 12, B1-AppD] The `read_ranges` bounded-concurrency reader was zero-caller AND used `buffer_unordered` (completion-order output that can't be paired back to input ranges). Add `read_ranges` to the `IcebergStorage`/`SendIcebergStorage` traits (default = sequential, order-preserving), override on S3 with the parallel path fixed to `buffered` (ORDER-PRESERVING) and switch-gated, and wire it into both sparse-Parquet coalesced-fetch loops (`io/parquet.rs` and the Send path `io/send_parquet.rs`, replacing the sequential `for` loop that the code's own comment flagged "could be parallelized"). Non-S3 backends (incl. the local-file wrapper) keep the correct sequential default. Kill switch: `FLUREE_ICEBERG_PARALLEL_RANGE_GETS` (default on; OFF = the byte-identical pre-item-12 sequential fetch). No new corpus member — the existing cold members exercise this path; PR-HARNESS's cold-subset re-run is its evidence. Also commits the scan-side gate record (docs/audit-impl/cov-scan-gates.md).
The item-8 ASC top-k `set_topk(var, k, true)` arm (operator_tree.rs) trips the workspace `[lints.clippy] semicolon_if_nothing_returned = deny`; the scan-side clippy run predated the arm, so it landed red on the branch. One-char `;`, no behavior change — surfaced by the fold-side `cargo clippy -p fluree-db-query`.
… 9b, F-AUD-8] Two coverage widenings to the single-`GRAPH`/single-source fused-aggregate fold, both riding the EXISTING `FLUREE_FUSED_R2RML_AGG` master switch. They are committed together because both add fields to the shared `Resolved` struct + its two constructions, so neither compiles without the other. Per A2's kill-switch lesson: switch-OFF reverts BOTH widenings (back to the pre-PR full-materialize decline). [9] MIN/MAX fused admission (F-AUD-8). Adds `AggregateFn::Min`/`Max` to the fold: a new `Fold::MinMax`/`Acc::MinMax` materializes ONLY the candidate object term (no subject IRI, no per-row BindingRow) via the same `materialize_object_from_batch` + `LiteralEncoder` path the FILTER fold uses, and keeps the running extreme by `compare_bindings` — so the result is byte-identical to the generic `agg_min`/ `agg_max`, but STREAMING (O(1) memory) instead of buffering every value. Scoped to numeric (int/long/double/decimal) + date/timestamp PLAIN-LITERAL columns (`minmax_admissible_datatype` + a `TermType::Literal`/no-lang gate); string (collation) and un-annotated (→ xsd:string) columns decline. Grouped + ungrouped. No manifest/column-stats shortcut in this PR (fold-only — the ungrouped-stats shortcut is a documented follow-on). Corpus: q075 (ungrouped) + q076 (grouped, 4 channels), blessed native oracles. [9b] Filtered-COUNT fused admission — the q038 class (D-c5 slice 1.5). The single-table path previously DECLINED any `star_constraints` (a constant-object flag like `isCurrent true`) to avoid an over-count, forcing q038's constrained COUNT to full-materialize at ~7k rows/s (57.6s virtual / 301x native). It now resolves each constraint to a per-row scalar-column check and APPLIES it in the fold, via the SAME `resolve_star_constraint_checks` / `row_satisfies_constraints` machinery the join path (E2) already uses — a constrained COUNT excludes the non-matching (and NULL-column) rows exactly as the materialized answer does. A constraint that does not resolve to a scalar column still declines (no silent over-count), and the COUNT(*) manifest shortcut stays declined for a constraint-bearing plan (it requires `fact_constraints.is_empty()` — the delete- blind `record_count` cannot see per-row matches). Removes the now-obsolete `fold_over_star_constraints` guard + its "must-decline" tripwire test, replaced by `slice_1_5_admits_and_applies_a_single_table_flag_constraint` and `multi_constraint_requires_all_to_match` (AND-semantics). Corpus: q077 multi- constraint COUNT (isCurrent ∧ segment=Enterprise = 50038, blessed native oracle); the existing q022/q061 grouped members remain the over-count tripwires (they now FUSE with the constraint applied — oracle unchanged). Gates + oracles recorded in docs/audit-impl/cov-scan-gates.md (fold-side section).
…s [audit item 14] C2's manifest-backed /info stats route was gated on an empty native shell (`t == 0`), so `get_data_model` lost the virtual model the moment a virtual-dataset shell received any native write (`t > 0`). Item 14 makes routing per-member: a graph-source-registered ledger serves its manifest-backed virtual stats regardless of the native `t`. An empty shell (t == 0) serves virtual-only (unchanged); a HYBRID (t > 0 + a graph source) builds BOTH members and merges — native ledger/commit/index metadata stays authoritative and native-only classes/properties are preserved, while the graph-source member's classes are overlaid by IRI (the graph source wins a shared IRI; a UNION, never a SUM, so no double-count) and its `source` block (snapshot + counts) is attached. Bypasses the response cache (hybrids are rare); a deserialize failure falls back to the virtual model (fail-safe). New default-on switch `FLUREE_R2RML_INFO_MEMBER_ROUTING` (off = the strict t==0 reroute). Keeps the existing `FLUREE_ICEBERG_INFO_COUNT_BUDGET_MS` time-budget semantics. MoR rider (#1520 F-AUD-1): `fetch_virtual_table_row_counts` derives per-table counts from the snapshot summary `total-records`, which OVER-COUNTS a merge-on-read table (it does not subtract position/equality deletes — recognized but not yet applied). It now flags such a table via `mor_guard::summary_indicates_deletes` (zero I/O) into a new `mor-approximate-tables` list on the `source` block, so the count is reported as an honest upper bound rather than silently authoritative — matching the existing `StatsCompleteness.has_delete_files` convention. Consumers that ignore the new key (the /data-model + MCP readers, all null-safe per C-adversarial O4) are unaffected. Hermetic tests: routing default-on parse, the hybrid class-union merge (graph source wins, no double-count, native metadata kept), and the MoR upper-bound flag surfacing. Gate record: docs/audit-impl/cov-scan-gates.md (fold-side).
The MIN/MAX fold replaced its running extreme only on `Ordering::Greater`
(FIRST-on-ties), but the generic `agg_max` (`max_by`) keeps the LAST of equal
maximums. For values that compare Equal yet render differently — a double column
holding both `+0.0` and `-0.0` ("0" vs "-0") — that picked the wrong element and
broke the byte-parity claim. Extracted `minmax_should_replace(is_max, ord)`: MAX
now replaces on `ord != Less` (last-on-ties, == max_by); MIN stays `== Less`
(first-on-ties, == min_by). Candidates are already materialized before the
compare, so replacing on `Equal` costs no extra work. New unit test
`minmax_tie_break_matches_generic_agg` cross-checks the fold against agg_min/agg_max
on the ±0.0 case (fails under the old first-on-ties MAX).
…ure predicate Extract the bare-COUNT manifest-shortcut eligibility (one CountRows fold, no FILTER / GROUP BY / folded constant-object constraints) out of the inline `next_batch` condition into a pure `count_shortcut_eligible` fn, and add a direct unit test. This is the D-c5 soundness line for item 9b: a constraint-bearing COUNT (e.g. `isCurrent true`) MUST decline the delete-blind `record_count` shortcut and count matching rows in the fold instead — previously that decline was only verified by inspection (R-1522). The extraction is behavior-preserving (all existing fused-aggregate tests unchanged); the test pins the decline for the filtered / grouped / constrained / non-CountRows cases, so a future edit can't silently re-admit an over-counting shortcut.
The committed virtual perf baseline was 62% gate-blind: 28 members sat pinned at exactly 180000ms — DNFs (deadline-capped walls) blessed AS the timeout, so a now-fast query could regress ~1740x (to 216s) before the 20%-over gate tripped. Root cause: write_perf blessed r.wall_ms regardless of status. Guard it with a pure is_dnf_wall(status, wall_ms, timeout_ms): a terminal Status::Dnf, or (belt-and-suspenders) any wall at/over the query's deadline, blesses as no-baseline (must-fix) — the matching wall field is cleared to None so a timeout cap can never masquerade as a budget, and a query that regressed to a DNF loses any stale fast wall. Corpus supplies each deadline; DNF counters still recorded. Two unit tests: the predicate, and the end-to-end no-baseline + stale-clear.
E1 (the ex:category shared-predicate class collapse) shipped unswitched on the audited line — fuse_class_if_safe called wildcard_class_fusion_is_safe directly with no kill switch, so an operator could not revert it in the field (A2 D2). Add a dedicated FLUREE_R2RML_SHARED_PREDICATE_FUSION (default on). OFF does not re-materialize: control falls through to the weaker pre-E1 class_prune_hint (star + separate class scan, still correct), exactly the pre-E1 behavior. Kept distinct from FLUREE_R2RML_CRAWL_CLASS_FUSION (the browse-crawl wildcard path) so the two mechanisms are not conflated. The existing ON-path test still passes; the OFF path targets an already-tested fallback. W4-1/W4-1b are documented exemptions in SWITCHES.md (coarse-revertible via PREDICATE_PUSHDOWN; a dedicated switch is invasive — they share the baseline pushdown apply site).
The two existing C3 tests cancel up front on an empty stream — they prove the first checkpoint fires but not that a drain loop is stopped mid-way. Add a test where a 5-batch stream cancels the query while producing its first batch: the loop must consume exactly one batch and then return Cancelled at the next checkpoint, leaving four unread (poll count 1, not 5). This is the actual 'cancellation stops a drain loop' property the audit flagged as uncovered. Coverage note: R3-B (r3b_scan_window/parent_build_budget_aborts_typed), PR-8 429/Retry-After (retries_on_429_then_succeeds + backoff-bound, wiremock), and C2 /info routing (#1522 info_member_routing/merge_virtual/mor_approximate) are already covered by #1521/#1522 — this closes the one remaining C3 gap.
…(F-AUD-19) SWITCHES.md documented ~19 of the ~40 on-path FLUREE_* switches and omitted PR-8 (catalog concurrency/retries/backoff/disk-cache/parallel-catalog), PR-2 (footer-from-cache/scan-concurrency), C1 (dataset-budget), C4 (rowgroup-parallelism), R3-B (memory-budget-bytes), and the #1520/#1521/#1522 additions — an operator could not revert those from the doc. Regenerated by enumerating every FLUREE_* read across query/iceberg/api and verifying each row against code (A2 is the map; code is truth). Adds the ~8 A2 omissions + the 9 new switches (incl. the inverted-polarity ALLOW_MOR_DELETES escape hatch and the new SHARED_PREDICATE_FUSION), sections for correctness/safety/capacity and baseline levers, the W4-1/W4-1b unswitched exemptions, and fixes the CRAWL_CLASS_FUSION name drift (A2 D2). Format: name | default | gates | OFF restores | introducing PR.
The live virtual re-bless surfaced 4 members (q038, q056/q057/q059) aborting with MemoryBudgetExceeded (a Status::Error 507, ~8.6-9.4GB) under #1521's default-on FLUREE_SCAN_MEM_ACCOUNTING — its cumulative-no-decrement counter conflates total-rows-streamed with resident memory and false-aborts long bounded-window streams (q038 completes in 52.5s with accounting off; it completed in C2). The never-bless-a-DNF guard did NOT catch these: an error is not a Status::Dnf and its wall (time-to-abort) is under the deadline, so write_perf would have pinned the abort time as a perf budget. Generalize is_dnf_wall -> is_unblessable_wall: only a Status::Ok run finishing inside its deadline yields a blessable wall; DNF, Error, and ExpectedError all bless as no-baseline (must-fix). Tests extended with the error and expected-error cases.
…budget The drift set (q002/q004/q022/q024/q030/q043) is network-flappy Snowflake catalog latency, not engine (C2 §3/§4 measured up to ~3.8x run-to-run swing). Give each a 300% per-query override so a catalog-latency spike (backed by the compare auto-rerun) does not flap the gate, while a gross (>4x) regression still trips.
…8, item 2a) Full-corpus native-sf01 run (77 members, 3 reps, local ledger, no creds) at the audit head. Native perf baseline goes 54 -> 77 entries with ZERO timeout caps (the never-bless-a-non-completion guard held; native has no DNFs). The 74 correctness oracles all reproduced exactly (3 expected-virtual-error skips) — no native regression. q077 (doubly-constrained COUNT) = 65.9s native release, a native-planner cost class.
…gate record (F-AUD-18, item 2b) The committed virtual perf baseline was 62% gate-blind (33 members pinned at 180000ms — DNF caps blessed as budgets, blessed_from 7d77218). Re-blessed from a full live virtual-sf01 corpus run, spliced with a post-#1521-fix subset re-run: vbench compare --run <merged> --gate -> 77 records, 0 hash mismatches, 0 perf violations, exit 0. Now 77 entries, ZERO at 180000; the only no-baselines are the 3 expected-errors (q013/q034/q051) + 3 memory-abort members not in the re-run subset (q056/q057/q059). q038 blessed at its real 58.2s wall (the #1521 window-release fix confirmed live — no abort). Splice methodology + memory-abort finding + q038-fusion-PARTIAL diagnosis + new-member wall table recorded in docs/audit-impl/cov-scan-gates.md; the three PAT-scrubbed JSONLs ship in audit-2026-07/data/.
…ot 33) + drift-residual note R-1523 nonblocking: the stale virtual-sf01.json had 28 members at hot=180000ms + 2 at 120000ms (q044/q050), not 33 (an earlier grep over-counted string occurrences). Also document the accepted drift-override residual: the 300% budgets on the 6 catalog-dominated drift members tolerate a sub-4x engine regression there (hash arm unaffected) — a deliberate anti-flap trade-off.
The graph-source subgraph-crawl expander formatted OBJECT-position bindings with
the flat-select JSON-LD formatter, which forks off before the format dispatch.
That produced three native-parity defects on virtual-dataset browse:
- D1: an FK/ref object serialized as a bare string, not {"@id": ...}, killing
forward graph navigation (the ref @id no longer string-matched the target
subject's @id).
- D2: format: typed-json was silently ignored on the whole crawl path.
- D3: an xsd:boolean object (which the R2RML operator leaves as a string)
rendered as "true" instead of the JSON boolean true.
Replace the two format_binding_with_result call sites with a node-document-aware
facade format::format_node_object_binding: it wraps object IRIs as
{"@id": compact_id_iri(...)} using the SAME id-compactor the crawl uses for the
root @id (so ref ids are byte-identical to subject ids), dispatches literals on
format.format (typed-json reaches typed.rs), and coerces a stringly boolean back
to a real bool in both formats -- anchored on what native hydration emits. Root
@id/@type handling is unchanged.
Ships UNSWITCHED: these are parity BUG fixes; a switch preserving the
silently-wrong/divergent output has negative value (DEC-002).
Tests: the two ref substring asserts become parsed-shape asserts; add a
typed-json variant, a boolean-shape test, and the /info<->crawl invariant (every
property /info types as @id must serialize as {"@id":...}).
The R2RML rewrite refused a virtual-dataset query carrying unconvertible
patterns with a generic InvalidQuery(String) -> HTTP 400 and no machine code,
and its prose falsely blamed "bound subjects ... or bound objects" -- both of
which actually convert and run (a bound subject `<iri> ?p ?o`, and a
constant-predicate bound object `?s ex:name "value"`). Solo had to prose-match a
message that was both undifferentiated and wrong.
Add a structured QueryError::R2rmlUnsupportedPattern { detail } raised at both
rewrite refusal sites (seeded + batched) via a shared
r2rml_unsupported_pattern_error() helper, so the message cannot drift. The
server maps it to a distinct, stable @type machine code
err:r2rml/UnsupportedPattern (new fluree-vocab R2RML_UNSUPPORTED_PATTERN) while
KEEPING HTTP 400 -- DEC-002 ruling: a well-formed request that is unsupported on
this source, not a distinct status like the 403/507 precedents. The streaming
ndjson path carries the matching code "r2rml_unsupported_pattern".
Corrected prose names the ACTUAL trigger (a VARIABLE predicate paired with a
BOUND term, e.g. `?s ?p <iri>`) and states which shapes DO work. The Display
keeps the "cannot be converted to R2RML scans" phrase existing prose-matchers
rely on during migration.
Ships UNSWITCHED (parity/correctness fix; DEC-002). Solo gates on the body
@type err:r2rml/UnsupportedPattern at HTTP 400.
Tests: server test asserts 400 + the distinct @type + the migration substring.
The subgraph-crawl planner (detect_wildcard_crawl / build_flat_query) never reads a top-level `values` clause, so a crawl select-map carrying VALUES had it SILENTLY DROPPED and returned the ENTIRE class instead of the VALUES-constrained subjects -- a wrong-answer defect (the handoff's proposed VALUES workaround returns ~390k rows live), strictly worse than a refusal. Guard expand_wildcard_crawl: once a query is recognized as a crawl this module handles, a present top-level `values` is refused with the typed QueryError::R2rmlUnsupportedPattern envelope (err:r2rml/UnsupportedPattern, HTTP 400) BEFORE any scan, naming the working WHERE-clause alternative. The loud-or-correct precedent: silent-wrong is the one unacceptable class. Honoring VALUES on the crawl path is deferred (C2). A flat-select VALUES query is not a crawl select-map, so it is detected None and flows unchanged to the normal path where VALUES already works -- regression- guarded. Ships UNSWITCHED (correctness fix; DEC-002). Tests: crawl + top-level VALUES -> typed refusal with zero scans (not whole-class results); flat-select VALUES -> crawl expander declines (Ok(None)).
…sMaps A subgraph crawl whose @type names a class that ZERO TriplesMaps declare (e.g. `@type Commit` on an Iceberg mapping) left the injected wildcard scan unconstrained, fanning out over every TriplesMap -- a 16-table wildcard scan DNF -- instead of returning the fast empty result the query logically has (no subject can carry an unmapped class). In try_fuse_wildcard_class, when find_maps_for_class(class) is empty, fuse the (unsatisfiable) class_filter onto the wildcard anyway: the operator then resolves it to zero TriplesMaps and returns empty with NO scans. The vertical-partition safety check is moot for an unmapped class (no class-declaring map exists whose sibling could be dropped), so it is skipped only for that case -- a mapped-but-unsafe class still runs the safety check and fans out as before. Ships UNSWITCHED (correctness/perf fix; DEC-002). Does not touch legitimate wildcard crawls (no @type), multi-class datasets, or class fusion for mapped classes. Tests: unmapped-class crawl -> empty result with zero scanned tables; mapped class still scans exactly its class table.
The #1505 SecretRef insertion (f305df4) placed FlureeBuilder::with_secret_resolver BETWEEN build()'s doc comment and its #[cfg(feature = "native")] attribute, so: - both #[cfg(native)] and #[cfg(iceberg)] bound with_secret_resolver, over-gating the no-native BYO-IAM SecretRef surface behind native (breaking solo's per-lambda fast path, which is iceberg-without-native); - build() was left cfg-less, so a no-native compile pulled in its native-only body (storage_path / FileStorage / FileNameService) and failed to build. Move with_secret_resolver (its own doc + #[cfg(iceberg)]) after build(), rejoining build()'s doc + #[cfg(native)]. with_secret_resolver is now gated on iceberg ONLY. Verified: cargo check -p fluree-db-api --no-default-features --features aws,iceberg,shacl (solo's real combination) FAILED before and PASSES after; native default + iceberg still builds. The no-native featureset check is now a permanent gate -- --all-features and workspace builds structurally mask feature-gating bugs via unification. Rides the browse-parity wave as a separate commit (DEC-002 scope addition, lead-verified 2026-07-20). The in-place #1505 patch decision belongs to that PR's owner; this tip fix unblocks solo immediately.
…lass)
Close the F-AUD-8 886x-PARTIAL residual: the ungrouped, direct-path filtered
COUNT `SELECT (COUNT(*)) WHERE { ?s a edw:Customer ; edw:isCurrent true }` (q038)
materialized at ~7k rows/s (57.6s live) while its grouped sibling q022 and the
var-object star already fused.
Trace verdict: the suspected empty-GROUP-BY cost guard is NOT the cause. A
constant-object triple stays a Pattern::Triple (SPARQL lowering never desugars
`isCurrent true` to `?v` + FILTER), so `filter` is None at detection and the
guard never fires -- q038 is admitted. The real decline is downstream: when a
subject-star carries no variable-object member, the R2RML rewrite emits the
class scan and each const-object constraint as SEPARATE R2rml patterns
(rewrite.rs, the var_members.is_empty() branch), which miss the single-[R2rml]
fused gate in resolve_at_open and fall to a materialize. The multi-constraint
FROM-path sibling q077 has the same no-var-object shape and did not truly fuse
either -- this fix closes both.
Fix (form a -- reuse, not new machinery): combine_constrained_class_scan
recombines that same-subject split back into one class-scan-carrying-
star_constraints pattern -- byte-identical to what the rewrite already folds for
the var-object case (q022) -- so the single-table 9b fold applies each
constraint per row via the existing resolve_star_constraint_checks +
row_satisfies_constraints. It runs before the join arm (a subject-star is not an
FK chain). The cost guard is left intact and documented: it still declines a
genuine residual FILTER, which is a different shape.
Soundness (D-c5, over-count = the one unacceptable outcome): admits ONLY the
exact shape (one subject var, one pure class scan, scalar const-object
equalities); every other shape declines to the existing arms.
resolve_star_constraint_checks still declines any RefObjectMap/non-scalar
constraint to the materialize fallback, and the resulting non-empty
fact_constraints keeps the delete-blind manifest COUNT shortcut declined. No
new constraint source.
Switch: rides FLUREE_FUSED_R2RML_AGG as a widening (DEC-002 policy). With the
fold OFF, detect_fused_r2rml_aggregate returns None, the fused operator is never
built, and q038 reverts to the materialized path.
Corpus: add q078 (Graph-path multi-constraint COUNT -- the direct-path twin of
q077) plus its native oracle (current Enterprise customers = 50038, result-hash
identical to q077). q038's existing native oracle is unchanged.
Refs: audit F-AUD-8, DEC-002.
…(Cluster B)
A select-map with a constant-IRI key (`{"select": {"<iri>": ["*"]}}`, also the
`["@type"]` and forward-predicate-list forms) lowered to Root::Sid and hydrated
against the (absent) native binary index, returning the bare `{"@id": <iri>}`
stub for an EXISTING virtual subject (deployed "subject detail is empty" bug, D4).
Route a constant-IRI root through the proven bound-subject wildcard scan
(`{@id: <iri>, ?p: ?o}`) instead: the R2RML operator prunes it to the subject's
own table via subject-template reversal, and the Cluster-A node-object facade
shapes FK refs as {"@id":…} and literals per format. One pruned scan serves every
projection (applied at assembly), avoiding the inner-join drop a per-predicate
scan suffers when a requested predicate is absent. Absent/unreversible subject →
the native-parity `[{"@id": <iri>}]` stub (native's missing-subject contract).
Behind FLUREE_R2RML_SELECT_MAP_ROUTING (default on) — an insurance switch, since
this re-routes an existing shipping-but-broken path; OFF restores the stub. The
native-ledger path is untouched (maybe_expand_crawl's graph-source gate).
…wl (E1)
The property-scoped browse crawl (`{"select": {"?s": ["*"]}, "where": {"@id":
"?s", "<prop>": "?v"}, "limit": N}`) DNF'd (>90s, D7): it lowers to a selective
const-predicate R2RML scan (`?s <prop> ?v`) plus a variable-predicate FULL-SOURCE
wildcard (`?s ?p ?o`) sharing ?s. Both estimated equal
(DEFAULT_PROPERTY_SCAN_SELECTIVITY), so reorder kept the rewrite's emit order
[wildcard, prop] — leaving the wildcard as the INNER scan with an empty seed,
i.e. an UNBUDGETED full scan over every TriplesMap. The LIMIT budget landed on
the outer const-pred scan and never reached the driving wildcard.
Estimate a pruning-key-less variable-predicate R2RML scan (no bound subject,
class, class-prune hint, or pinned TriplesMap) as FULL_SCAN, so reorder places it
LAST: the selective const-pred scan drives (inner), the wildcard becomes the
LIMIT-budgeted correlated OUTER. Sound — reordering two co-subject scans
preserves the solution set (the top LIMIT truncates the same rows; every driving
subject has >=1 triple so the budget never under-fills); it only lets the
existing budget reach a scan that then terminates. The residual cross-table
over-scan (the correlated wildcard still resolves every TriplesMap) is the
deferred bound-object prune (X2), not a correctness issue.
Behind FLUREE_R2RML_BUDGET_PROPERTY_VAR (default on); OFF reverts to the equal
estimate (byte-identical results, unbudgeted browse).
…B parity) Native's select-map hydration omits @id from an explicit projection that does not request it — `{"<iri>": ["@type"]}` returns {"@type": …} (no @id), and a forward-predicate list returns only the requested predicates. Only the wildcard (and an explicit `"@id"`) carry @id. The initial Cluster B seeded @id unconditionally, which would have hash-mismatched the native oracle for the ["@type"] and property-list forms. Track an explicit `"@id"` request (CrawlProjection::Predicates.want_id) and emit @id in expand_bound_subject_select_map only for the wildcard / id-only forms and when want_id. The variable-subject crawl is unchanged (it always emits @id as the node identity). Caught by blessing the native oracles for q081/q082.
…map harness path
Promote the P3 browse shape-matrix into 7 corpus members and teach the vbench
harness to run JSON-LD select-map crawls (the only way to exercise the crawl
expansion / node-document hydration path — the corpus was SPARQL-only).
Harness: exec.rs detects a JSON-LD body (first non-comment line begins with '{')
and runs it through .jsonld() (crawl expansion on virtual, native hydration on
native) instead of .sparql(); the node-document array result is canonicalized by
the existing canon bare-array branch (key-sorted multiset, so key/node order is
parity-safe). A leading '#' comment header is stripped before parse.
Members (native oracles blessed offline from native-sf01): q079 class-page crawl
(Cluster A, rows_only), q080 subject-detail ["*"] + q081 ["@type"] + q082
property-list (Cluster B, hash-gated Full), q083 property-scoped listing (E1,
rows_only), q084 filtered page (rows_only), q085 inbound-edge count (works-today
shape 9, Full). New closed Tag::Browse; smoke covers it via q080; corpus count,
rows_only, and tag-cover meta-tests updated (78 -> 85 queries, 24 tags).
q038's native leg reproduces UNCHANGED (sanity). Gate record in
docs/audit-impl/browse-parity-gates.md.
…ss-engine The live subset FAILED-PARITY on q080 (subject-detail crawl) only. Root cause was the parity gate, not Cluster B: canon::canonicalize_graph hashed each JSON-LD node with serde_json::to_string WITHOUT sorting its keys — it sorts the row multiset but not keys within a node. Native hydration emits a node's properties in alphabetical key order; the R2RML crawl emits them in table-column order. q080 was byte-for-byte content-identical, only reordered — a false mismatch. q080 is the first Full-gated cross-engine node-document comparison the corpus asserts (the only prior graph members, q048/q049 CONSTRUCT, are rows_only; the canon comment noted node hash equality was 'not yet asserted'). Recursively sort object keys before serializing each node (array / @list element order preserved — only object keys are sorted). Re-bless q080's oracle with the corrected canon (--force); q081/q082 were already alphabetical so unchanged, and the SPARQL-path oracles are untouched. Test: node_key_order_does_not_affect_hash. Live re-run on the corrected canon: compare --gate = 11 records, 0 hash mismatches, 0 perf violations. Full table in docs/audit-impl/browse-parity-gates.md.
… fused-aggregate comment
R-PARITY nonblocking follow-ups (verdict: SHIP).
(1) Cluster B: add the absent-subject parity test for the NON-wildcard select-map
forms. Native returns [{}] (one empty node) for both a missing subject's
["@type"] and a forward-predicate list — an explicit projection does not seed
@id, and no requested property/type is present, but a node IS emitted for the
requested root. VERIFIED against native-sf01 (a missing customer); Cluster B
already produces the same, so this pins the parity (the wildcard-absent form
keeps its [{"@id"}] stub).
(2) fused_aggregate.rs: correct a stale comment (~:1751). It claimed a
constant-object flag DECLINES before the group-key default loop; post-E2/F1
combine_constrained_class_scan FUSES it onto the class scan as a star_constraints
entry that is APPLIED per row (resolve_star_constraint_checks /
row_satisfies_constraints), so it is applied, not declined. Comment-only touch to
base code — a comment now stating the opposite of behavior is a landmine for the
next reader of the group-key default's soundness argument.
Gates (touched crates): fmt clean; clippy 0 new lints; fluree-db-query all bins
0 failed; fluree-db-api lib 821 + grp_graphsource 122, 0 failed.
…ily-C) Lift the join-path blanket filter decline in `resolve_join_at_open` (fused_aggregate.rs) and port the single-table `FilterPlan` machinery to the join path, closing the two deployed production DNFs (P4-famc-probe.md): Q1 (SupportTicket⋈Customer, fact-side `?status != "Closed"`) and Q2 (InventorySnapshot⋈Product, var-to-var `?onHand < ?reorder`, COUNT+2×AVG), which before this widening declined and materialized the whole join (~10.3s / ~5.0s virtual at SF0.1, DNF at deployed scale). The FILTER is routed to the single pattern that owns every referenced variable (`route_filter_source`): a FACT-side residual is carried in `Resolved.filter` and applied per fact row in `next_batch` (the exact machinery the single-table path already uses — extracted into the shared `build_filter_plan` so a filtered join is byte-parity with the single-table fold); a TERMINAL-dim residual is applied per dim row during the FK→GKey map build (`row_passes_filter_plan`), so a fact row probing a filtered-out dim key drops. Multi-AVG folds are unchanged (`resolve_agg_folds` already folds several). Soundness (the D-c5 bar — a wrong count is the one unacceptable outcome): the fused filter matches the materialized `FilterOperator` semantics by construction — all three (materialized operator, `passes_filters`, fused FilterPlan) evaluate the SAME `PreparedBoolExpression` via `eval_to_bool_non_strict`, which demotes a can-demote error to `false` (row excluded). A NULL/absent filter-member column is an unbound BGP member ⇒ the row is EXCLUDED (it does NOT count as "not Closed"): on the fact side this is the R2RML star row-validity (`validity_cols`, which fires before the filter — fact filter vars are always fact object vars); on the dim side `row_passes_filter_plan` enforces the same null-drop inline (a null column short-circuits to `false`), keeping `!BOUND`/COALESCE-style filters sound. Every decline is enumerated in code: a var-free filter; a var bound as an object on ≥2 patterns or a filter spanning ≥2 patterns; an interior-dim owner (v1, symmetric with the interior-dim group-key decline); a RefObjectMap/template/non-scalar var; and EXISTS/subquery filters (detection only captures a bare `Pattern::Filter`). Switch: rides `FLUREE_FUSED_R2RML_AGG_JOIN` as a widening — the join sub-switch OFF ⇒ `resolve_join_at_open` is never reached ⇒ a filtered join reverts to materialize. Tests: admission matrix (`route_filter_source`), scalar-admit/ref-decline (`build_filter_plan`), NULL-exclusion + comparison semantics with null-bearing fixtures (`row_passes_filter_plan` — the D-c5 crux), and constraint+filter conjunction. Gate record in docs/audit-impl/family-c-gates.md.
…s members Add q086 (open-tickets-by-segment, fact-side inequality FILTER over the SupportTicket⋈Customer join = deployed DNF Q1) and q087 (below-reorder-by-category, fact-side var-to-var FILTER + COUNT/2×AVG over InventorySnapshot⋈Product = deployed DNF Q2), promoted from the P4-famc probe corpus. Both are Full hash-gated (deterministic grouped results), so the fused virtual answer must match the native materialized oracle byte-for-byte — the family-C soundness gate at corpus level. Native oracles blessed offline against native-sf01 (no creds): q086 = 3 rows (hash 69006c4ffd39…, byte-identical to P4's measured p1), q087 = 10 rows (hash d7aba410bb32…, byte-identical to P4's measured p2, AVG decimals included). The native path never touches the fused R2RML join, so these oracles are the materialized ground truth unchanged by the widening. corpus_version 4→5, count 85→87; smoke still covers every tag (filter_string / filter_range / join / fk_chain are all pre-covered). SF01 data has no NULL STATUS/onHand rows, so these members exercise the non-null path; the NULL-exclusion semantics are covered by the hermetic unit tests in fused_aggregate.rs.
…t-key decline (R-1528) Two nonblocking hardening items from the R-1528 review (SHIP verdict), both on the family-C branch: 1. Fact-side NULL defense / filter-eval symmetry. `next_batch`'s fact filter now routes through the same `row_passes_filter_plan` helper the dim side uses, so a NULL/absent filter-member column EXCLUDES the row explicitly (BGP row-drop) instead of leaking through a demotable `Binding::Unbound`. Today this is unreachable — `validity_cols` null-drops the fact filter's member columns before the filter runs — and behavior-identical on every reachable input (`materialize_object_from_batch` over a scalar-column ObjectMap, all `build_filter_plan` produces, never returns Err; only the Ok(None) arm differs, which validity makes unreachable). It makes that invariant fail-safe: if a future refactor ever eroded the validity coverage, a null member would still exclude the row (never counted as, e.g., "not Closed") rather than leak. Restores one filter-eval path across single-table, fact-join, and dim. 2. Duplicate-parent-key guard. `insert_dim_gkeys` now DECLINES the fused plan on ANY duplicate parent join key (previously it kept an equal-value duplicate). A non-unique parent join key means the materialized inner join FANS OUT (one fact row matches multiple dim rows), which a single-entry-per-key map cannot represent: conflicting group attrs mis-attribute (last-wins) and even equal group attrs under-count the fan-out. Declining (→ materialize) is the conservative posture the operator already takes for out-of-fold shapes. Proper star schemas have unique parent PKs (the RefObjectMap parent columns are the dim surrogate key), so this never fires there — no corpus regression. Tests: `family_c_fact_filter_null_member_excludes_failsafe` (Q1 STATUS shape, bypasses validity by calling the helper directly) and the renamed `dim_dup_join_key_always_declines` (deliberately non-unique parent fixture). Gates (-p fluree-db-query): fmt clean; clippy 0 new; 1320 lib + all bins, 0 failed.
…convergence) Resolve the scan-files cache-hit arms family-c-side: family-c's guard-aware `guard_cached_scan_files` supersedes lambda's `servable_scan_files` decline — same end-state semantics (flagged + no override refuses with a typed error; flagged + override serves with a caveat). r2rml.rs converges byte-identical to family-c's scan-files region; the orphaned `servable_scan_files` helper, its miss-arm probe, and its unit test are dropped — family-c's `cached_scan_files_guard_refuses_delete_bearing_entry` covers the same case with a stronger (typed-refusal) assertion. B1 `insert_dim_gkeys` (doc + body) and the refreshed "ANY duplicate" caller comments auto-resolve to the corrected f17 wording. disk_catalog_cache.rs and cancellation.rs are byte-identical across both heads; lambda's #1500 §1 loadTable tests carry over verbatim. hash_join.rs UNION resolution: lambda's D1 boundary-release test (d1_rebuild_boundary_release_no_false_abort) is adapted to family-c's membudget era — its per-build charge is derived from BINDING_EST_BYTES (size_of::<Binding>() = 88 here) instead of the removed flat-64 assumption, so its two assertions (charge retained until released; no false-abort after the boundary release) are preserved against family-c's constant.
…formed-counter test (#1520 W1/W2) Absorb #1520's W1/W2 fail-closed guard into family-c's re-applied mor_guard.rs. `summary_delete_evidence` reads the raw summary map: a present counter that is non-zero, negative, or unparseable refuses (naming key+value); an absent one proceeds (manifest-list backstop). `ensure_no_summary_deletes` and the reconciled `summary_indicates_deletes` route through it — one fail-closed definition serving the guard, the planner's ScanPlan.has_delete_manifests stamp, the /info row-count path, and override-read caveats. W1 tests (garbage / -1 / absent / classifier) and the W2 `error_message_is_actionable` retarget (planner location + preview "qualified (location)" shapes) ported. /info side: extract `metadata_indicates_mor_approximate_count` (the exact predicate `fetch_virtual_table_row_counts` uses) and pin the 1528-W1 outcome — a malformed or negative delete counter lists the table in mor-approximate-tables (row count reported as an upper bound), never counted as exact; a clean append snapshot is not flagged.
perf(virtual): big-Iceberg-audit implementation — correctness/capacity guards, coverage widenings, harness re-bless, browse-parity, family-C (consolidates the audit program + #1525)
PR #1528 (big-Iceberg-audit implementation + browse-parity + family-C, approved by bplatz) was merged while its base still pointed at the scaffolding branch perf/audit-mainmerge-base, so its content landed there instead of main. This merges current main (carrying #1520, #1507, #1514, #1477) into the audit content so the combined tree can be proposed to main. Conflict resolutions: - fluree-db-iceberg/src/mor_guard.rs (add/add): family-C converged superset. Its W1/W2 fail-closed guard bodies are byte-identical to main's; it omits the ensure_summary_scannable / ensure_manifests_scannable planner helpers, which the family-C planners inline. Verified those helpers have no remaining callers. - fluree-db-iceberg/src/lib.rs, scan/planner.rs, scan/send_planner.rs: family-C inlined-guard arms (+ has_delete_manifests ScanPlan stamp). Main's non-conflict additions (parse_manifest_list_with_deletes, the F-AUD-1 mor_guard_tests fixtures) auto-merged and were preserved. - fluree-db-api/src/graph_source/iceberg_catalog.rs: main's W3-evolved preview stats guard (typed 409 error, hoisted allow_mor, table_display, preview_counts_are_upper_bounds) — a functional superset of the audit's older F-AUD-1 form. Its preview_summary_guard called the dropped ensure_summary_scannable; inlined that helper's body (mor_deletes_allowed + ensure_no_summary_deletes), matching how the family-C planners inline the same guard. - fluree-db-api/src/format/mod.rs: family-C crawl formatter (crawl uses format_node_object_binding; the format_binding_with_result re-export is unused). Gated the crawl-only formatter helpers and their Binding import behind the iceberg feature to clear a pre-existing #1528 featureless dead-code gap. - fluree-db-api/src/ledger_info.rs: auto-merged as the union of main's #1534 redaction machinery and the audit /info mor-approximate extensions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#1528 (bplatz-approved) was merged while its base still pointed at scaffolding branch
perf/audit-mainmerge-base, so its content landed atc81862d2ainstead ofmain. This branch merges currentmain(#1520/#1507/#1514/#1477) into that content as a gated, conflict-resolved two-parent merge (0703484f8) so the approved work reachesmain.One intentional API-shape note a reviewer will spot: this lands family-C's 3-arg ASC-capable
set_topk(var, k, ascending)overmain's older 2-arg DESC-only form — verified tree-consistent (trait + all impls + all callers), with no #1477 feature loss (the fast-path kill switch, per-predicate counts, dedup-safety lane, differential harness, and OFFSET decline all coexist in the merged tree).All local gates pass at the merge head (core 704, query 1327, api featureless 673 + iceberg 828, iceberg all-features 251 — zero failures); CI runs on this PR since base=main. Per-conflict resolutions plus independent-verifier corroboration are recorded in
audit-2026-07/review-response-2/pr-1528-verification-and-convergence.md§RECOVER-1528.