From f28300df0954a8a59102a4318fd9f451e57673ab Mon Sep 17 00:00:00 2001 From: bplatz Date: Sat, 25 Apr 2026 10:05:02 -0400 Subject: [PATCH] bind iri / sid op, t in queries --- docs/api/endpoints.md | 12 +- docs/concepts/time-travel.md | 20 +- docs/getting-started/quickstart-query.md | 14 +- docs/query/jsonld-query.md | 13 +- docs/query/sparql.md | 6 +- docs/transactions/retractions.md | 6 +- fluree-db-api/src/format/agent_json.rs | 4 +- fluree-db-api/src/format/construct.rs | 6 +- fluree-db-api/src/format/delimited.rs | 24 +- fluree-db-api/src/format/graph_crawl.rs | 4 +- fluree-db-api/src/format/jsonld.rs | 4 +- fluree-db-api/src/format/materialize.rs | 8 +- fluree-db-api/src/format/sparql.rs | 4 +- fluree-db-api/src/format/sparql_xml.rs | 4 +- fluree-db-api/src/format/typed.rs | 4 +- fluree-db-api/src/policy_builder.rs | 2 +- fluree-db-api/tests/it_query_history_range.rs | 283 ++++++++++++++---- .../tests/it_upsert_duplicate_ids_repro.rs | 4 +- fluree-db-cli/src/output.rs | 6 +- fluree-db-query/src/binary_scan.rs | 43 ++- fluree-db-query/src/binding.rs | 241 +++++++++++---- fluree-db-query/src/bm25/operator.rs | 4 +- fluree-db-query/src/dataset_operator.rs | 2 +- fluree-db-query/src/expression/compare.rs | 4 +- fluree-db-query/src/expression/eval.rs | 4 +- fluree-db-query/src/expression/fluree.rs | 34 ++- fluree-db-query/src/expression/helpers.rs | 2 +- fluree-db-query/src/expression/rdf.rs | 8 +- fluree-db-query/src/expression/types.rs | 4 +- fluree-db-query/src/expression/value.rs | 4 +- .../src/fast_group_count_firsts.rs | 19 +- fluree-db-query/src/fast_label_regex_type.rs | 2 +- .../src/fast_star_const_order_topk.rs | 2 +- fluree-db-query/src/filter.rs | 2 +- fluree-db-query/src/geo_search.rs | 2 +- fluree-db-query/src/group_aggregate.rs | 65 ++-- fluree-db-query/src/groupby.rs | 20 +- fluree-db-query/src/ir.rs | 6 +- fluree-db-query/src/join.rs | 46 +-- fluree-db-query/src/materializer.rs | 22 +- fluree-db-query/src/minus.rs | 44 +-- fluree-db-query/src/object_binding.rs | 30 +- fluree-db-query/src/optional.rs | 28 +- fluree-db-query/src/parse/lower.rs | 2 +- fluree-db-query/src/parse/node_map.rs | 139 +++++---- fluree-db-query/src/property_join.rs | 46 ++- fluree-db-query/src/property_path.rs | 18 +- fluree-db-query/src/s2_search.rs | 16 +- fluree-db-query/src/sort.rs | 40 +-- fluree-db-query/src/stats_query.rs | 4 +- fluree-db-query/src/values.rs | 4 +- fluree-db-query/src/vector/operator.rs | 2 +- fluree-db-query/tests/correctness_tests.rs | 2 +- .../tests/groupby_aggregate_tests.rs | 28 +- ...owl2rl_property_rules_integration_tests.rs | 4 +- .../tests/values_bind_union_tests.rs | 2 +- fluree-db-sparql/src/lower/describe.rs | 2 +- fluree-db-sparql/src/lower/term.rs | 2 +- fluree-db-transact/src/generate/flakes.rs | 6 +- fluree-db-transact/src/stage.rs | 22 +- 60 files changed, 877 insertions(+), 528 deletions(-) diff --git a/docs/api/endpoints.md b/docs/api/endpoints.md index 80b8a7a08..6eded67b2 100644 --- a/docs/api/endpoints.md +++ b/docs/api/endpoints.md @@ -989,16 +989,18 @@ Query the history of entities using the standard `/query` endpoint with `from` a ``` The `@t` and `@op` annotations capture transaction metadata: -- **@t** - Transaction time when the value was asserted or retracted -- **@op** - Operation type: `"assert"` or `"retract"` +- **@t** - Transaction time (integer) when the fact was asserted or retracted. +- **@op** - Operation type as a boolean: `true` for assertions, `false` for retractions. (Mirrors `Flake.op` on disk; constants `"assert"` / `"retract"` are not accepted.) + +Both annotations work uniformly for literal-valued and IRI-valued objects. **Response:** ```json [ - ["Alice", 30, 1, "assert"], - ["Alice", 30, 5, "retract"], - ["Alicia", 31, 5, "assert"] + ["Alice", 30, 1, true], + ["Alice", 30, 5, false], + ["Alicia", 31, 5, true] ] ``` diff --git a/docs/concepts/time-travel.md b/docs/concepts/time-travel.md index 283dac0a4..1af9cdae0 100644 --- a/docs/concepts/time-travel.md +++ b/docs/concepts/time-travel.md @@ -114,13 +114,13 @@ History queries capture both the retraction and assertion with `@op`: ```json [ - [25, 1, "assert"], - [25, 5, "retract"], - [26, 5, "assert"] + [25, 1, true], + [25, 5, false], + [26, 5, true] ] ``` -Each row shows `[value, transaction_time, operation]`. +Each row shows `[value, transaction_time, op]` where `op` is `true` for assertions and `false` for retractions. ### Valid Time vs Transaction Time @@ -257,16 +257,16 @@ Track all changes to a specific entity over time by specifying a time range: ``` The `@t` and `@op` annotations bind the transaction time and operation type: -- **@t** - Transaction time when the fact was asserted or retracted -- **@op** - Either `"assert"` or `"retract"` +- **@t** - Transaction time (integer) when the fact was asserted or retracted. +- **@op** - Boolean: `true` for assertions, `false` for retractions. Mirrors `Flake.op` on disk. Both literal- and IRI-valued objects carry the metadata. Returns results showing all changes: ```json [ - ["Alice", 1, "assert"], - ["Alice", 5, "retract"], - ["Alicia", 5, "assert"] + ["Alice", 1, true], + ["Alice", 5, false], + ["Alicia", 5, true] ] ``` @@ -561,7 +561,7 @@ Use history queries to identify when a specific change happened: } ``` -The results show when `ex:status` changed, with `"retract"` for the old value and `"assert"` for the new value at the same transaction time. +The results show when `ex:status` changed, with `?op = false` (retract) for the old value and `?op = true` (assert) for the new value at the same transaction time. ### Audit Trail for Compliance diff --git a/docs/getting-started/quickstart-query.md b/docs/getting-started/quickstart-query.md index c970cbfbd..643f44e01 100644 --- a/docs/getting-started/quickstart-query.md +++ b/docs/getting-started/quickstart-query.md @@ -304,15 +304,15 @@ curl -X POST http://localhost:8090/v1/fluree/query \ }' ``` -The `@t` annotation binds the transaction time and `@op` shows the operation type (`"assert"` or `"retract"`). +The `@t` annotation binds the transaction time, and `@op` binds the operation type as a boolean (`true` = assert, `false` = retract). Response shows all changes: ```json [ - ["Alice", 30, 1, "assert"], - ["Alice", 30, 5, "retract"], - ["Alicia", 31, 5, "assert"] + ["Alice", 30, 1, true], + ["Alice", 30, 5, false], + ["Alicia", 31, 5, true] ] ``` @@ -342,9 +342,9 @@ Response: ```json [ - [30, 1, "assert"], - [30, 5, "retract"], - [31, 5, "assert"] + [30, 1, true], + [30, 5, false], + [31, 5, true] ] ``` diff --git a/docs/query/jsonld-query.md b/docs/query/jsonld-query.md index 3362896d9..e8c045ae1 100644 --- a/docs/query/jsonld-query.md +++ b/docs/query/jsonld-query.md @@ -801,8 +801,10 @@ History queries let you see all changes (assertions and retractions) within a ti Use `@t` and `@op` annotations on value objects to capture metadata: -- **@t** - Binds the transaction time when the fact was asserted/retracted -- **@op** - Binds the operation type: `"assert"` or `"retract"` +- **@t** - Binds the transaction time (integer) when the fact was asserted/retracted. +- **@op** - Binds the operation type as a boolean: `true` for assertions, `false` for retractions. (Mirrors `Flake.op` on disk; constants `"assert"` / `"retract"` are *not* accepted — use `true` / `false`.) + +Both annotations work uniformly for literal-valued and IRI-valued objects. **Entity History:** @@ -851,6 +853,8 @@ Use `@t` and `@op` annotations on value objects to capture metadata: **Filter by Operation:** +You can either use a constant `@op` shorthand (preferred) or filter on the bound variable: + ```json { "@context": { "ex": "http://example.org/ns/" }, @@ -858,12 +862,13 @@ Use `@t` and `@op` annotations on value objects to capture metadata: "to": "ledger:main@t:latest", "select": ["?name", "?t"], "where": [ - { "@id": "ex:alice", "ex:name": { "@value": "?name", "@t": "?t", "@op": "?op" } }, - ["filter", "(= ?op \"retract\")"] + { "@id": "ex:alice", "ex:name": { "@value": "?name", "@t": "?t", "@op": false } } ] } ``` +The shorthand `"@op": false` lowers to `FILTER(op(?name) = false)`. Equivalent long form using a bound variable: `"@op": "?op"` plus `["filter", "(= ?op false)"]`. + **All Properties History:** ```json diff --git a/docs/query/sparql.md b/docs/query/sparql.md index fd4163ee9..42248fb6a 100644 --- a/docs/query/sparql.md +++ b/docs/query/sparql.md @@ -836,8 +836,8 @@ ORDER BY ?t ``` The `<< subject predicate object >>` syntax (RDF-star) treats the triple as an entity that can have metadata: -- `f:t` - Transaction time when the fact was asserted or retracted -- `f:op` - Operation type: `"assert"` or `"retract"` +- `f:t` - Transaction time (integer) when the fact was asserted or retracted. +- `f:op` - Operation type as a boolean: `true` for assertions, `false` for retractions. Mirrors `Flake.op` on disk. **Filter by operation type:** @@ -851,7 +851,7 @@ TO WHERE { << ex:alice ex:age ?age >> f:t ?t . << ex:alice ex:age ?age >> f:op ?op . - FILTER(?op = "retract") + FILTER(?op = false) } ``` diff --git a/docs/transactions/retractions.md b/docs/transactions/retractions.md index cd181d6de..cb757b7ba 100644 --- a/docs/transactions/retractions.md +++ b/docs/transactions/retractions.md @@ -345,12 +345,12 @@ curl -X POST http://localhost:8090/v1/fluree/query \ Response: ```json [ - ["Alice", 1, "assert"], - ["Alice", 5, "retract"] + ["Alice", 1, true], + ["Alice", 5, false] ] ``` -The `@t` annotation captures the transaction time and `@op` shows whether each value was asserted or retracted. +The `@t` annotation captures the transaction time and `@op` binds a boolean — `true` for assertions, `false` for retractions (mirroring `Flake.op` on disk). ## Error Handling diff --git a/fluree-db-api/src/format/agent_json.rs b/fluree-db-api/src/format/agent_json.rs index aea5dc3d2..5a6ff873a 100644 --- a/fluree-db-api/src/format/agent_json.rs +++ b/fluree-db-api/src/format/agent_json.rs @@ -224,7 +224,9 @@ fn format_row_with_types( fn binding_type_label(binding: &Binding, compactor: &IriCompactor) -> Result> { match binding { Binding::Unbound | Binding::Poisoned => Ok(None), - Binding::Sid(_) | Binding::IriMatch { .. } | Binding::Iri(_) => Ok(Some("uri".to_string())), + Binding::Sid { .. } | Binding::IriMatch { .. } | Binding::Iri(_) => { + Ok(Some("uri".to_string())) + } Binding::EncodedSid { .. } | Binding::EncodedPid { .. } => Ok(Some("uri".to_string())), Binding::Lit { dtc, .. } => { if dtc.lang_tag().is_some() { diff --git a/fluree-db-api/src/format/construct.rs b/fluree-db-api/src/format/construct.rs index 4366d4439..b66a900f2 100644 --- a/fluree-db-api/src/format/construct.rs +++ b/fluree-db-api/src/format/construct.rs @@ -134,7 +134,7 @@ fn resolve_subject_term( }; match binding { - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { let expanded_iri = compactor.decode_sid(sid)?; Ok(Some(IrTerm::iri(expanded_iri))) } @@ -207,7 +207,7 @@ fn resolve_predicate_term( }; match binding { - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { let expanded_iri = compactor.decode_sid(sid)?; Ok(Some(IrTerm::iri(expanded_iri))) } @@ -304,7 +304,7 @@ fn binding_to_ir_term( Binding::Unbound | Binding::Poisoned => Ok(None), // Reference - IRI (expanded) - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { let expanded_iri = compactor.decode_sid(sid)?; Ok(Some(IrTerm::iri(expanded_iri))) } diff --git a/fluree-db-api/src/format/delimited.rs b/fluree-db-api/src/format/delimited.rs index cf06e7dfa..416186ee1 100644 --- a/fluree-db-api/src/format/delimited.rs +++ b/fluree-db-api/src/format/delimited.rs @@ -370,7 +370,7 @@ fn write_binding_cell( Binding::Unbound | Binding::Poisoned => { // Empty cell } - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { write_compacted_sid(cell, compactor, sid)?; } Binding::IriMatch { iri, .. } => { @@ -384,7 +384,7 @@ fn write_binding_cell( Binding::Lit { val, .. } => { write_flake_value(cell, val, compactor); } - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { let gv = require_graph_view(gv)?; let store = gv.store(); let iri = store.resolve_subject_iri(*s_id).map_err(|e| { @@ -659,7 +659,7 @@ mod tests { fn test_tsv_sid_binding_no_context() { // Without @context, Sid outputs full IRI (no compaction possible) let snapshot = make_test_snapshot(); - let result = make_result(&["?s"], vec![vec![Binding::Sid(Sid::new(100, "alice"))]]); + let result = make_result(&["?s"], vec![vec![Binding::sid(Sid::new(100, "alice"))]]); let tsv = format_tsv(&result, &snapshot).unwrap(); assert_eq!(tsv, "s\nhttp://example.org/alice\n"); } @@ -670,7 +670,7 @@ mod tests { let snapshot = make_test_snapshot(); let result = make_result_with_context( &["?s"], - vec![vec![Binding::Sid(Sid::new(100, "alice"))]], + vec![vec![Binding::sid(Sid::new(100, "alice"))]], make_test_context(), ); let tsv = format_tsv(&result, &snapshot).unwrap(); @@ -713,7 +713,7 @@ mod tests { let snapshot = make_test_snapshot(); let result = make_result( &["?a", "?b"], - vec![vec![Binding::Sid(Sid::new(100, "x")), Binding::Unbound]], + vec![vec![Binding::sid(Sid::new(100, "x")), Binding::Unbound]], ); let tsv = format_tsv(&result, &snapshot).unwrap(); assert_eq!(tsv, "a\tb\nhttp://example.org/x\t\n"); @@ -725,9 +725,9 @@ mod tests { let result = make_result( &["?s"], vec![ - vec![Binding::Sid(Sid::new(100, "a"))], - vec![Binding::Sid(Sid::new(100, "b"))], - vec![Binding::Sid(Sid::new(100, "c"))], + vec![Binding::sid(Sid::new(100, "a"))], + vec![Binding::sid(Sid::new(100, "b"))], + vec![Binding::sid(Sid::new(100, "c"))], ], ); let tsv = format_tsv(&result, &snapshot).unwrap(); @@ -743,9 +743,9 @@ mod tests { let result = make_result( &["?s"], vec![ - vec![Binding::Sid(Sid::new(100, "a"))], - vec![Binding::Sid(Sid::new(100, "b"))], - vec![Binding::Sid(Sid::new(100, "c"))], + vec![Binding::sid(Sid::new(100, "a"))], + vec![Binding::sid(Sid::new(100, "b"))], + vec![Binding::sid(Sid::new(100, "c"))], ], ); let (tsv, total) = format_tsv_limited(&result, &snapshot, 2).unwrap(); @@ -882,7 +882,7 @@ mod tests { let snapshot = make_test_snapshot(); let result = make_result_with_context( &["?s"], - vec![vec![Binding::Sid(Sid::new(100, "alice"))]], + vec![vec![Binding::sid(Sid::new(100, "alice"))]], make_test_context(), ); let csv = format_csv(&result, &snapshot).unwrap(); diff --git a/fluree-db-api/src/format/graph_crawl.rs b/fluree-db-api/src/format/graph_crawl.rs index 2c979f907..ccabdd645 100644 --- a/fluree-db-api/src/format/graph_crawl.rs +++ b/fluree-db-api/src/format/graph_crawl.rs @@ -237,12 +237,12 @@ pub async fn format_async( let materialized = super::materialize::materialize_binding(result, binding)?; match materialized { - Binding::Sid(sid) => Some(sid), + Binding::Sid { sid, .. } => Some(sid), Binding::IriMatch { primary_sid, .. } => Some(primary_sid), _ => None, } } - Some(Binding::Sid(sid)) => Some(sid.clone()), + Some(Binding::Sid { sid, .. }) => Some(sid.clone()), Some(Binding::IriMatch { primary_sid, .. }) => Some(primary_sid.clone()), Some(Binding::Unbound | Binding::Poisoned) | None => None, Some( diff --git a/fluree-db-api/src/format/jsonld.rs b/fluree-db-api/src/format/jsonld.rs index f3f924e84..d543c3ef1 100644 --- a/fluree-db-api/src/format/jsonld.rs +++ b/fluree-db-api/src/format/jsonld.rs @@ -82,7 +82,7 @@ pub(crate) fn format_binding(binding: &Binding, compactor: &IriCompactor) -> Res Binding::Unbound | Binding::Poisoned => Ok(JsonValue::Null), // Reference (IRI or blank node) - compact using @context - Binding::Sid(sid) => Ok(JsonValue::String(compactor.compact_sid(sid)?)), + Binding::Sid { sid, .. } => Ok(JsonValue::String(compactor.compact_sid(sid)?)), // IriMatch: use canonical IRI, then compact (multi-ledger mode) Binding::IriMatch { iri, .. } => Ok(JsonValue::String(compactor.compact_iri(iri)?)), @@ -387,7 +387,7 @@ mod tests { #[test] fn test_format_binding_sid() { let compactor = make_test_compactor(); - let binding = Binding::Sid(Sid::new(100, "alice")); + let binding = Binding::sid(Sid::new(100, "alice")); let result = format_binding(&binding, &compactor).unwrap(); // Without @context, returns full IRI assert_eq!(result, json!("http://example.org/alice")); diff --git a/fluree-db-api/src/format/materialize.rs b/fluree-db-api/src/format/materialize.rs index 7eff9f91f..7b8c607e0 100644 --- a/fluree-db-api/src/format/materialize.rs +++ b/fluree-db-api/src/format/materialize.rs @@ -41,13 +41,13 @@ fn materialize_encoded_binding( ) -> std::io::Result { let store = gv.store(); match binding { - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { let iri = store.resolve_subject_iri(*s_id)?; let sid = store.encode_iri(&iri); - Ok(Binding::Sid(sid)) + Ok(Binding::sid(sid)) } Binding::EncodedPid { p_id } => match store.resolve_predicate_iri(*p_id) { - Some(iri) => Ok(Binding::Sid(store.encode_iri(iri))), + Some(iri) => Ok(Binding::sid(store.encode_iri(iri))), None => Err(std::io::Error::new( std::io::ErrorKind::NotFound, format!("Unknown predicate ID: {p_id}"), @@ -75,7 +75,7 @@ fn materialize_encoded_lit(binding: &Binding, gv: &BinaryGraphView) -> std::io:: let store = gv.store(); let val = gv.decode_value_from_kind(*o_kind, *o_key, *p_id, *dt_id, *lang_id)?; match val { - FlakeValue::Ref(sid) => Ok(Binding::Sid(sid)), + FlakeValue::Ref(sid) => Ok(Binding::sid(sid)), other => { let dt_sid = store .dt_sids() diff --git a/fluree-db-api/src/format/sparql.rs b/fluree-db-api/src/format/sparql.rs index 872d8a5dc..b7d4abf68 100644 --- a/fluree-db-api/src/format/sparql.rs +++ b/fluree-db-api/src/format/sparql.rs @@ -133,7 +133,7 @@ fn format_binding( Binding::Unbound | Binding::Poisoned => Ok(None), // Reference (IRI or blank node) - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { // SPARQL JSON output uses compact IRIs where possible (not full IRIs). let iri = compactor.compact_sid(sid)?; // Check if it's a blank node (starts with _:) @@ -480,7 +480,7 @@ mod tests { fn test_format_binding_uri() { let compactor = make_test_compactor(); let result = make_test_result(); - let binding = Binding::Sid(Sid::new(100, "alice")); + let binding = Binding::sid(Sid::new(100, "alice")); let formatted = format_binding(&result, &binding, &compactor) .unwrap() .unwrap(); diff --git a/fluree-db-api/src/format/sparql_xml.rs b/fluree-db-api/src/format/sparql_xml.rs index 635e8cfb4..6457bd35c 100644 --- a/fluree-db-api/src/format/sparql_xml.rs +++ b/fluree-db-api/src/format/sparql_xml.rs @@ -215,7 +215,7 @@ fn format_binding_xml( match binding { Binding::Unbound | Binding::Poisoned => Ok(None), - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { let iri = compactor.decode_sid(sid)?; Ok(Some(if iri.starts_with("_:") { let mut s = String::from(""); @@ -384,7 +384,7 @@ mod tests { let schema = std::sync::Arc::from(vec![s_var].into_boxed_slice()); let sid = Sid::new(100, "alice"); - let batch = Batch::single_row(schema, vec![Binding::Sid(sid)]).unwrap(); + let batch = Batch::single_row(schema, vec![Binding::sid(sid)]).unwrap(); result.batches = vec![batch]; let xml = format(&result, &compactor, &FormatterConfig::sparql_xml()).unwrap(); diff --git a/fluree-db-api/src/format/typed.rs b/fluree-db-api/src/format/typed.rs index ac2429e3d..28c225a57 100644 --- a/fluree-db-api/src/format/typed.rs +++ b/fluree-db-api/src/format/typed.rs @@ -81,7 +81,7 @@ pub(crate) fn format_binding( Binding::Unbound | Binding::Poisoned => Ok(JsonValue::Null), // Reference - use @id notation - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { let iri = compactor.compact_sid(sid)?; Ok(json!({"@id": iri})) } @@ -348,7 +348,7 @@ mod tests { fn test_format_binding_sid() { let compactor = make_test_compactor(); let result = make_test_result(); - let binding = Binding::Sid(Sid::new(100, "alice")); + let binding = Binding::sid(Sid::new(100, "alice")); let formatted = format_binding(&result, &binding, &compactor).unwrap(); assert_eq!(formatted, json!({"@id": "http://example.org/alice"})); } diff --git a/fluree-db-api/src/policy_builder.rs b/fluree-db-api/src/policy_builder.rs index 5167f1157..bf3cc32ad 100644 --- a/fluree-db-api/src/policy_builder.rs +++ b/fluree-db-api/src/policy_builder.rs @@ -822,7 +822,7 @@ async fn query_predicate( for flake in flakes { match flake.o { - FlakeValue::Ref(sid) => results.push(Binding::Sid(sid)), + FlakeValue::Ref(sid) => results.push(Binding::sid(sid)), val => { let dtc = match flake .m diff --git a/fluree-db-api/tests/it_query_history_range.rs b/fluree-db-api/tests/it_query_history_range.rs index a23069682..b0dcf8ce5 100644 --- a/fluree-db-api/tests/it_query_history_range.rs +++ b/fluree-db-api/tests/it_query_history_range.rs @@ -3,7 +3,8 @@ //! Reporter scenario: a query with explicit `"from"`/`"to"` keys (e.g. //! `"from": "ledger@t:1", "to": "ledger@t:latest"`) should emit every //! assert and retract event with `t` in that range, and the `@op` -//! binding should resolve to `"assert"` or `"retract"` per event. +//! binding should resolve to `true` (assert) or `false` (retract) +//! per event — mirroring `Flake.op` on disk. //! //! Before the fix: //! - The binary cursor only emitted currently-asserted base rows, so @@ -44,7 +45,8 @@ async fn reindex_to_current(fluree: &fluree_db_api::Fluree, ledger_id: &str) -> } /// History-range query should emit assert + retract events from the -/// history sidecar, with `@op` bound to `"assert"` / `"retract"`. +/// history sidecar, with `@op` bound to `true` (assert) / `false` +/// (retract). /// /// Sequence: /// - t=1: insert `ex:alice ex:name "Alice"` @@ -119,37 +121,14 @@ async fn history_range_emits_sidecar_events_with_op() { let value = serde_json::to_value(&result.result).expect("serialize"); let rows = value.as_array().expect("rows array").clone(); - // Helper: flatten one formatted row `{"?v": ..., "?t": ..., "?op": ...}` - // into `(v_str, t_i64, op_str)` so assertions are easy to read. - fn flatten(row: &serde_json::Value) -> (String, i64, String) { - let v = row - .get("?v") - .and_then(|x| x.get("@value")) - .and_then(|x| x.as_str()) - .unwrap_or_default() - .to_string(); - let t = row - .get("?t") - .and_then(|x| x.get("@value")) - .and_then(serde_json::Value::as_i64) - .unwrap_or(-1); - let op = row - .get("?op") - .and_then(|x| x.get("@value").or(Some(x))) - .and_then(|x| x.as_str()) - .unwrap_or("null") - .to_string(); - (v, t, op) - } - - let flattened: Vec<(String, i64, String)> = rows.iter().map(flatten).collect(); - // orderBy (?t, ?op, ?v) with lexicographic ordering: - // "assert" < "retract", so at t=2 the assert of "Alice Smith" comes - // before the retract of "Alice". - let expected: Vec<(String, i64, String)> = vec![ - ("Alice".to_string(), 1, "assert".to_string()), - ("Alice Smith".to_string(), 2, "assert".to_string()), - ("Alice".to_string(), 2, "retract".to_string()), + let flattened: Vec<(String, i64, bool)> = rows.iter().map(flatten_v_t_op).collect(); + // orderBy (?t, ?op, ?v): false (retract) sorts before true (assert) + // numerically, so at t=2 the retract of "Alice" precedes the assert + // of "Alice Smith". + let expected: Vec<(String, i64, bool)> = vec![ + ("Alice".to_string(), 1, true), + ("Alice".to_string(), 2, false), + ("Alice Smith".to_string(), 2, true), ]; assert_eq!( flattened, expected, @@ -161,8 +140,8 @@ async fn history_range_emits_sidecar_events_with_op() { // Helpers shared with the coverage cases below // --------------------------------------------------------------------------- -/// Flatten a formatted row into `(?v, ?t, ?op)` strings. -fn flatten_v_t_op(row: &serde_json::Value) -> (String, i64, String) { +/// Flatten a formatted row into `(?v: String, ?t: i64, ?op: bool)`. +fn flatten_v_t_op(row: &serde_json::Value) -> (String, i64, bool) { let v = row .get("?v") .and_then(|x| x.get("@value")) @@ -177,16 +156,15 @@ fn flatten_v_t_op(row: &serde_json::Value) -> (String, i64, String) { let op = row .get("?op") .and_then(|x| x.get("@value").or(Some(x))) - .and_then(|x| x.as_str()) - .unwrap_or("null") - .to_string(); + .and_then(serde_json::Value::as_bool) + .expect("?op should be a boolean"); (v, t, op) } async fn run_history_query( fluree: &fluree_db_api::Fluree, q: &serde_json::Value, -) -> Vec<(String, i64, String)> { +) -> Vec<(String, i64, bool)> { let result = fluree .query_from() .jsonld(q) @@ -285,10 +263,10 @@ async fn history_range_novelty_only() { }); let rows = run_history_query(&fluree, &q).await; - let expected: Vec<(String, i64, String)> = vec![ - ("Alice".to_string(), 1, "assert".to_string()), - ("Alice Smith".to_string(), 2, "assert".to_string()), - ("Alice".to_string(), 2, "retract".to_string()), + let expected: Vec<(String, i64, bool)> = vec![ + ("Alice".to_string(), 1, true), + ("Alice".to_string(), 2, false), + ("Alice Smith".to_string(), 2, true), ]; assert_eq!(rows, expected, "novelty-only history must also bind @op"); } @@ -296,7 +274,7 @@ async fn history_range_novelty_only() { // --------------------------------------------------------------------------- // Case: `@op` as a constant filter — asserts only. // -// The parser lowers `{"@op": "assert"}` into `FILTER(op(?v) = "assert")`. +// The parser lowers `{"@op": true}` into `FILTER(op(?v) = true)`. // That filter runs downstream of the scan, so the history operator // just needs to emit rows with op populated and the FILTER does the rest. // --------------------------------------------------------------------------- @@ -326,7 +304,7 @@ async fn history_range_op_constant_filter_assert() { .expect("tx2"); assert_eq!(reindex_to_current(&fluree, ledger_id).await, 2); - // Ask only for asserts. `@op: "assert"` is a FILTER constant, not a + // Ask only for asserts. `@op: true` is a FILTER constant, not a // BIND — `?op` never exists as a variable, so select only `?v`/`?t` // and assert the filter returns both assert events and no retracts. let q = json!({ @@ -336,7 +314,7 @@ async fn history_range_op_constant_filter_assert() { "select": ["?v", "?t"], "where": [{ "@id": "ex:alice", - "ex:name": {"@value": "?v", "@t": "?t", "@op": "assert"} + "ex:name": {"@value": "?v", "@t": "?t", "@op": true} }], "orderBy": ["?t", "?v"], }); @@ -345,7 +323,7 @@ async fn history_range_op_constant_filter_assert() { vec![("Alice".to_string(), 1), ("Alice Smith".to_string(), 2)]; assert_eq!( rows, expected, - "@op=\"assert\" filter must return only assert events" + "@op=true filter must return only assert events" ); } @@ -382,7 +360,7 @@ async fn history_range_op_constant_filter_retract() { "select": ["?v", "?t"], "where": [{ "@id": "ex:alice", - "ex:name": {"@value": "?v", "@t": "?t", "@op": "retract"} + "ex:name": {"@value": "?v", "@t": "?t", "@op": false} }], "orderBy": ["?t", "?v"], }); @@ -390,7 +368,7 @@ async fn history_range_op_constant_filter_retract() { let expected: Vec<(String, i64)> = vec![("Alice".to_string(), 2)]; assert_eq!( rows, expected, - "@op=\"retract\" filter must return only retract events" + "@op=false filter must return only retract events" ); } @@ -443,12 +421,12 @@ async fn history_range_sidecar_plus_novelty_boundary() { "orderBy": ["?t", "?op", "?v"], }); let rows = run_history_query(&fluree, &q).await; - let expected: Vec<(String, i64, String)> = vec![ + let expected: Vec<(String, i64, bool)> = vec![ // t=1 assert comes from base (base t=1 ≤ persisted_to_t=1) - ("Alice".to_string(), 1, "assert".to_string()), - // t=2 assert+retract come from novelty ((index_t, to_t]) - ("Alice Smith".to_string(), 2, "assert".to_string()), - ("Alice".to_string(), 2, "retract".to_string()), + ("Alice".to_string(), 1, true), + // t=2 retract+assert come from novelty ((index_t, to_t]) + ("Alice".to_string(), 2, false), + ("Alice Smith".to_string(), 2, true), ]; assert_eq!( rows, expected, @@ -505,15 +483,200 @@ async fn history_range_subject_unbound() { "orderBy": ["?t", "?op", "?v"], }); let rows = run_history_query(&fluree, &q).await; - // t=1: Alice+assert, Bob+assert; t=2: Alice Smith+assert, Alice+retract - let expected: Vec<(String, i64, String)> = vec![ - ("Alice".to_string(), 1, "assert".to_string()), - ("Bob".to_string(), 1, "assert".to_string()), - ("Alice Smith".to_string(), 2, "assert".to_string()), - ("Alice".to_string(), 2, "retract".to_string()), + // t=1: Alice+assert, Bob+assert; t=2: Alice+retract, Alice Smith+assert + // (false = vec![ + ("Alice".to_string(), 1, true), + ("Bob".to_string(), 1, true), + ("Alice".to_string(), 2, false), + ("Alice Smith".to_string(), 2, true), ]; assert_eq!( rows, expected, "subject-unbound history must walk all matching leaflets" ); } + +// --------------------------------------------------------------------------- +// IRI-object regression coverage. +// +// The original fix only threaded `t` / `op` onto literal-valued objects. +// Ref-valued objects (rdf:type, foaf:knows, skos:inScheme, etc.) showed +// up in the result set with `?v` populated but `?t` and `?op` null, +// because `Binding::Sid` had no metadata channel. After making the Sid +// variant metadata-capable, the history scan must populate `t` / `op` +// for ref-valued objects too. +// --------------------------------------------------------------------------- + +/// Helper: flatten a row whose `?v` is an IRI into `(iri: String, t: i64, op: bool)`. +fn flatten_iri_v_t_op(row: &serde_json::Value) -> (String, i64, bool) { + let v = row + .get("?v") + .and_then(|x| x.get("@value").or(Some(x))) + .and_then(|x| x.get("@id").or(Some(x))) + .and_then(|x| x.as_str()) + .unwrap_or_default() + .to_string(); + let t = row + .get("?t") + .and_then(|x| x.get("@value")) + .and_then(serde_json::Value::as_i64) + .unwrap_or(-1); + let op = row + .get("?op") + .and_then(|x| x.get("@value").or(Some(x))) + .and_then(serde_json::Value::as_bool) + .expect("?op should be a boolean"); + (v, t, op) +} + +async fn run_iri_history_query( + fluree: &fluree_db_api::Fluree, + q: &serde_json::Value, +) -> Vec<(String, i64, bool)> { + let result = fluree + .query_from() + .jsonld(q) + .format(FormatterConfig::typed_json().with_normalize_arrays()) + .execute_tracked() + .await + .expect("history range query"); + let value = serde_json::to_value(&result.result).expect("serialize"); + value + .as_array() + .expect("rows array") + .iter() + .map(flatten_iri_v_t_op) + .collect() +} + +/// Sidecar + base case: `ex:knows` (ref-valued) over a span where the +/// initial assert lives in the persisted base columns and a later +/// retract+assert sit in the sidecar. Verifies that `?t` / `?op` are +/// populated identically for ref-valued and literal-valued objects. +#[tokio::test] +async fn history_range_iri_object_sidecar_plus_base() { + let tmp = tempfile::tempdir().expect("create temp dir"); + let fluree = FlureeBuilder::file(tmp.path().to_str().unwrap()) + .build() + .expect("build"); + let ledger_id = "test/history-iri-sidecar:main"; + let ledger0 = fluree.create_ledger(ledger_id).await.expect("create"); + + // t=1: alice knows bob (ref-valued). + let r1 = fluree + .insert( + ledger0, + &json!({ + "@context": ctx(), + "@graph": [ + {"@id": "ex:bob", "ex:name": "Bob"}, + {"@id": "ex:carol", "ex:name": "Carol"}, + {"@id": "ex:alice", "ex:knows": {"@id": "ex:bob"}}, + ], + }), + ) + .await + .expect("tx1"); + assert_eq!(r1.receipt.t, 1); + assert_eq!(reindex_to_current(&fluree, ledger_id).await, 1); + + // t=2: replace alice ex:knows bob → alice ex:knows carol. + // Upsert retracts the previous ref and asserts the new one. + let _ = fluree + .upsert( + r1.ledger, + &json!({ + "@context": ctx(), + "@id": "ex:alice", + "ex:knows": {"@id": "ex:carol"}, + }), + ) + .await + .expect("tx2"); + assert_eq!(reindex_to_current(&fluree, ledger_id).await, 2); + + let q = json!({ + "@context": ctx(), + "from": format!("{ledger_id}@t:1"), + "to": format!("{ledger_id}@t:latest"), + "select": ["?v", "?t", "?op"], + "where": [{ + "@id": "ex:alice", + "ex:knows": {"@value": "?v", "@type": "@id", "@t": "?t", "@op": "?op"} + }], + "orderBy": ["?t", "?op", "?v"], + }); + let rows = run_iri_history_query(&fluree, &q).await; + let expected: Vec<(String, i64, bool)> = vec![ + ("ex:bob".to_string(), 1, true), + ("ex:bob".to_string(), 2, false), + ("ex:carol".to_string(), 2, true), + ]; + assert_eq!( + rows, expected, + "history range over a ref-valued predicate must bind @t and @op" + ); +} + +/// Novelty-only case: same ref-valued predicate but with no reindex, +/// so all assert / retract events stay in novelty. Verifies the +/// novelty branch of the history collector also threads metadata +/// through the ref binding. +#[tokio::test] +async fn history_range_iri_object_novelty_only() { + let tmp = tempfile::tempdir().expect("create temp dir"); + let fluree = FlureeBuilder::file(tmp.path().to_str().unwrap()) + .build() + .expect("build"); + let ledger_id = "test/history-iri-novelty:main"; + let ledger0 = fluree.create_ledger(ledger_id).await.expect("create"); + + let r1 = fluree + .insert( + ledger0, + &json!({ + "@context": ctx(), + "@graph": [ + {"@id": "ex:bob", "ex:name": "Bob"}, + {"@id": "ex:carol", "ex:name": "Carol"}, + {"@id": "ex:alice", "ex:knows": {"@id": "ex:bob"}}, + ], + }), + ) + .await + .expect("tx1"); + let _ = fluree + .upsert( + r1.ledger, + &json!({ + "@context": ctx(), + "@id": "ex:alice", + "ex:knows": {"@id": "ex:carol"}, + }), + ) + .await + .expect("tx2"); + + let q = json!({ + "@context": ctx(), + "from": format!("{ledger_id}@t:1"), + "to": format!("{ledger_id}@t:latest"), + "select": ["?v", "?t", "?op"], + "where": [{ + "@id": "ex:alice", + "ex:knows": {"@value": "?v", "@type": "@id", "@t": "?t", "@op": "?op"} + }], + "orderBy": ["?t", "?op", "?v"], + }); + let rows = run_iri_history_query(&fluree, &q).await; + let expected: Vec<(String, i64, bool)> = vec![ + ("ex:bob".to_string(), 1, true), + ("ex:bob".to_string(), 2, false), + ("ex:carol".to_string(), 2, true), + ]; + assert_eq!( + rows, expected, + "novelty-only history over a ref-valued predicate must bind @t and @op" + ); +} diff --git a/fluree-db-api/tests/it_upsert_duplicate_ids_repro.rs b/fluree-db-api/tests/it_upsert_duplicate_ids_repro.rs index de70856ad..c9f04f711 100644 --- a/fluree-db-api/tests/it_upsert_duplicate_ids_repro.rs +++ b/fluree-db-api/tests/it_upsert_duplicate_ids_repro.rs @@ -244,13 +244,13 @@ async fn repro_upsert_repeated_ids_create_duplicate_subject_ids() { for row in 0..batch.len() { let b = batch.get_by_col(row, 0); match b { - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { let bg = bg.expect("EncodedSid requires binary_graph"); encoded_ids.push(*s_id); decoded_iris .push(bg.resolve_subject_iri(*s_id).expect("decode subject iri")); } - Binding::Sid(sid) => decoded_iris.push(sid_to_iri(sid, codes)), + Binding::Sid { sid, .. } => decoded_iris.push(sid_to_iri(sid, codes)), other => panic!("unexpected binding for ?m: {other:?}"), } } diff --git a/fluree-db-cli/src/output.rs b/fluree-db-cli/src/output.rs index 116c1f7b4..a9f540e65 100644 --- a/fluree-db-cli/src/output.rs +++ b/fluree-db-cli/src/output.rs @@ -162,7 +162,9 @@ fn sparql_table_cell( Binding::Unbound | Binding::Poisoned => String::new(), // Use display compaction (includes auto-derived fallback prefixes) - Binding::Sid(sid) => compact_bnode_strip(compactor.compact_sid_for_display(sid).ok()), + Binding::Sid { sid, .. } => { + compact_bnode_strip(compactor.compact_sid_for_display(sid).ok()) + } Binding::IriMatch { iri, .. } => { compact_bnode_strip(compactor.compact_iri_for_display(iri).ok()) } @@ -170,7 +172,7 @@ fn sparql_table_cell( Binding::Lit { val, .. } => flake_value_to_table_cell(val, compactor), - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { let Some(gv) = gv else { return Ok(format!("{b:?}")); }; diff --git a/fluree-db-query/src/binary_scan.rs b/fluree-db-query/src/binary_scan.rs index 18ed17669..ef4e06560 100644 --- a/fluree-db-query/src/binary_scan.rs +++ b/fluree-db-query/src/binary_scan.rs @@ -679,14 +679,23 @@ impl BinaryScanOperator { let mut bindings: Vec = vec![Binding::Unbound; base_len]; if let Some(pos) = self.s_var_pos.filter(|p| *p < base_len) { - bindings[pos] = Binding::Sid(flake.s.clone()); + bindings[pos] = Binding::sid(flake.s.clone()); } if let Some(pos) = self.p_var_pos.filter(|p| *p < base_len) { - bindings[pos] = Binding::Sid(flake.p.clone()); + bindings[pos] = Binding::sid(flake.p.clone()); } if let Some(pos) = self.o_var_pos.filter(|p| *p < base_len) { bindings[pos] = match &flake.o { - FlakeValue::Ref(r) => Binding::Sid(r.clone()), + // Object bindings carry the assertion time for both + // ref- and literal-valued flakes — `T(?v)` resolves + // uniformly. The `op` channel is only populated in + // history mode, where the scan emits both asserts + // and retracts; current-state scans only ever see + // asserts so the distinction would be misleading. + FlakeValue::Ref(r) if ctx.history_mode => { + Binding::sid_with_t_op(r.clone(), flake.t, flake.op) + } + FlakeValue::Ref(r) => Binding::sid_with_t(r.clone(), flake.t), v => { let dtc = match flake .m @@ -1178,14 +1187,14 @@ impl BinaryScanOperator { // Subject binding. if let Some(pos) = self.s_var_pos { let binding = if late_materialize { - Binding::EncodedSid { s_id } + Binding::encoded_sid(s_id) } else { // BinaryGraphView::resolve_subject_sid is novelty-aware: // novel subjects return Sid directly without IRI round-trip. let sid = view .resolve_subject_sid(s_id) .map_err(|e| QueryError::from_io("resolve_subject_sid", e))?; - Binding::Sid(sid) + Binding::sid(sid) }; if !Self::set_binding_at(&mut bindings, pos, binding) { continue; @@ -1197,7 +1206,7 @@ impl BinaryScanOperator { let binding = if late_materialize { Binding::EncodedPid { p_id } } else { - Binding::Sid(self.resolve_p_id(p_id)) + Binding::sid(self.resolve_p_id(p_id)) }; if !Self::set_binding_at(&mut bindings, pos, binding) { continue; @@ -1205,21 +1214,33 @@ impl BinaryScanOperator { } // Object binding. + // + // The non-history binary-scan path emits `op = None` — object + // ops only matter to history queries, which use the + // `BinaryHistoryScanOperator` collector path (see + // `flakes_to_bindings`). Threading `None` here keeps the + // metadata channel uniform for ref- and literal-valued + // objects without changing behaviour for current-state scans. if let Some(pos) = self.o_var_pos { let binding = if needs_o_decode || !late_materialize { let val = decoded_o.expect("decoded object required"); - materialized_object_binding(self.store(), o_type, p_id, val, t_opt) + materialized_object_binding(self.store(), o_type, p_id, val, t_opt, None) } else if let Some(encoded) = - late_materialized_object_binding(o_type, o_key, p_id, t_enc, o_i) + late_materialized_object_binding(o_type, o_key, p_id, t_enc, o_i, None) { encoded } else { // Fallback: decode if we don't have a safe encoded representation. // This preserves correctness for uncommon/custom OTypes. match decode_value(o_type, o_key, p_id) { - Ok(val) => { - materialized_object_binding(self.store(), o_type, p_id, val, t_opt) - } + Ok(val) => materialized_object_binding( + self.store(), + o_type, + p_id, + val, + t_opt, + None, + ), Err(e) => { return Err(QueryError::dictionary_lookup(format!( "binary scan object decode fallback failed: o_type={o_type}, o_key={o_key}, p_id={p_id}: {e}" diff --git a/fluree-db-query/src/binding.rs b/fluree-db-query/src/binding.rs index 41aadd3af..81568e258 100644 --- a/fluree-db-query/src/binding.rs +++ b/fluree-db-query/src/binding.rs @@ -41,7 +41,19 @@ pub enum Binding { /// /// Used in single-ledger mode where SID comparison is sufficient. /// For multi-ledger queries, prefer `IriMatch` which carries canonical IRI. - Sid(Sid), + /// + /// `t` and `op` carry history-mode metadata for ref-valued *object* + /// bindings (mirroring the Lit variant). Subject and predicate + /// bindings always set both to None — `T(?s)` / `OP(?p)` therefore + /// return null rather than inventing semantics for those positions. + /// Both fields are intentionally excluded from `PartialEq` and + /// `Hash` so set semantics (joins, DISTINCT, GROUP BY) ignore them, + /// exactly as for `Lit`. + Sid { + sid: Sid, + t: Option, + op: Option, + }, /// IRI reference with canonical IRI and per-ledger SID cache /// /// Used in multi-ledger (dataset) mode to ensure correct cross-ledger joins. @@ -97,8 +109,10 @@ pub enum Binding { /// - `dt_id`/`lang_id`/`i_val` provide literal metadata /// - `t` is the assertion transaction time (metadata) /// - /// NOTE: EncodedLit represents only literal values. References are still - /// represented as `Binding::Sid` (resolved via subject dictionaries). + /// NOTE: EncodedLit represents only literal values. References use + /// `Binding::Sid` (eagerly resolved via subject dictionaries) or + /// `Binding::EncodedSid` (late-materialised); both can carry the + /// same `t` / `op` history metadata that EncodedLit does. EncodedLit { o_kind: u8, o_key: u64, @@ -113,6 +127,11 @@ pub enum Binding { /// Used to defer subject dictionary lookups until join/output time. /// The `s_id` is the raw u64 from the binary index. /// + /// `t` and `op` mirror the metadata fields on `Sid` and are + /// populated only for ref-valued *object* bindings produced from a + /// flake in history mode. They are intentionally excluded from + /// `PartialEq` and `Hash`. + /// /// # Single-Ledger Only /// /// `EncodedSid` comparison by `s_id` is only valid within a single ledger. @@ -121,6 +140,10 @@ pub enum Binding { EncodedSid { /// Raw subject/ref ID from binary index s_id: u64, + /// Optional transaction time (history-mode object bindings only). + t: Option, + /// Optional operation type for history queries (true = assert, false = retract). + op: Option, }, /// Encoded predicate ID (late materialization). /// @@ -198,12 +221,90 @@ impl Binding { } } + /// Create a `Sid` binding without `t` / `op` metadata. + /// + /// Conventional constructor for subject and predicate bindings, + /// and any other ref binding that genuinely has no flake-scoped + /// metadata to carry (e.g. bindings synthesised from constants, + /// VALUES rows, or expression evaluation). + /// + /// Ref-valued *object* bindings emitted from a flake should use + /// [`Binding::sid_with_t`] (current-state scans) or + /// [`Binding::sid_with_t_op`] (history scans) so `T(?v)` / `OP(?v)` + /// resolve uniformly across literal- and IRI-valued predicates. + pub fn sid(sid: Sid) -> Self { + Binding::Sid { + sid, + t: None, + op: None, + } + } + + /// Create a `Sid` binding with assertion-time metadata only. + /// + /// Used by ref-valued *object* bindings emitted by non-history + /// scans, mirroring how `Binding::Lit` already carries `t` for + /// literal-valued objects. `op` stays `None` because the + /// assert/retract distinction is only meaningful in history mode + /// (current-state scans only see asserts). + pub fn sid_with_t(sid: Sid, t: i64) -> Self { + Binding::Sid { + sid, + t: Some(t), + op: None, + } + } + + /// Create a `Sid` binding with full history metadata. + /// + /// Used by ref-valued *object* bindings produced from a flake in + /// history mode. Subject and predicate bindings should use + /// `Binding::sid` instead. + pub fn sid_with_t_op(sid: Sid, t: i64, op: bool) -> Self { + Binding::Sid { + sid, + t: Some(t), + op: Some(op), + } + } + + /// Extract the transaction time metadata from a binding, if any. + /// + /// Centralises the variant list so callers (notably `eval_t` and + /// any future `T()` consumers) don't need to enumerate every + /// metadata-carrying variant. Subject/predicate `Sid` and + /// `EncodedSid` bindings always return `None` because they don't + /// carry per-flake `t` — the field is only populated for object + /// bindings emitted by the scan. + pub fn t(&self) -> Option { + match self { + Binding::Lit { t, .. } => *t, + Binding::EncodedLit { t, .. } => Some(*t), + Binding::Sid { t, .. } => *t, + Binding::EncodedSid { t, .. } => *t, + _ => None, + } + } + + /// Extract the operation type metadata from a binding, if any. + /// + /// Same rationale as `t()` — only populated for object bindings in + /// history mode. + pub fn op(&self) -> Option { + match self { + Binding::Lit { op, .. } => *op, + Binding::Sid { op, .. } => *op, + Binding::EncodedSid { op, .. } => *op, + _ => None, + } + } + /// Create a binding from a flake's object value /// /// Automatically routes `FlakeValue::Ref` to `Binding::Sid`. pub fn from_object(val: FlakeValue, dt: Sid) -> Self { match val { - FlakeValue::Ref(sid) => Binding::Sid(sid), + FlakeValue::Ref(sid) => Binding::sid(sid), other => Binding::Lit { val: other, dtc: DatatypeConstraint::Explicit(dt), @@ -219,7 +320,7 @@ impl Binding { /// Preserves language tags from `FlakeMeta.lang` for langString values. pub fn from_object_with_meta(val: FlakeValue, dt: Sid, meta: Option) -> Self { match val { - FlakeValue::Ref(sid) => Binding::Sid(sid), + FlakeValue::Ref(sid) => Binding::sid(sid), other => { let dtc = match meta.and_then(|m| m.lang.map(Arc::from)) { Some(lang) => DatatypeConstraint::LangTag(lang), @@ -242,7 +343,11 @@ impl Binding { /// as it preserves all metadata including the transaction time for `@t` bindings. pub fn from_object_with_t(val: FlakeValue, dt: Sid, meta: Option, t: i64) -> Self { match val { - FlakeValue::Ref(sid) => Binding::Sid(sid), + FlakeValue::Ref(sid) => Binding::Sid { + sid, + t: Some(t), + op: None, + }, other => { let dtc = match meta.and_then(|m| m.lang.map(Arc::from)) { Some(lang) => DatatypeConstraint::LangTag(lang), @@ -262,7 +367,9 @@ impl Binding { /// Create a binding from a flake's object value with full metadata including t and op. /// /// This is used for history mode queries where both the transaction time and - /// operation type (assert/retract) need to be captured. + /// operation type (assert/retract) need to be captured. Both literal and + /// ref-valued objects carry the metadata so the parser-generated + /// `BIND(t(?v) AS ?t)` / `BIND(op(?v) AS ?op)` patterns work uniformly. pub fn from_object_with_t_op( val: FlakeValue, dt: Sid, @@ -271,7 +378,7 @@ impl Binding { op: bool, ) -> Self { match val { - FlakeValue::Ref(sid) => Binding::Sid(sid), + FlakeValue::Ref(sid) => Binding::sid_with_t_op(sid, t, op), other => { let dtc = match meta.and_then(|m| m.lang.map(Arc::from)) { Some(lang) => DatatypeConstraint::LangTag(lang), @@ -288,6 +395,27 @@ impl Binding { } } + /// Create a raw `EncodedSid` binding without history metadata. + pub fn encoded_sid(s_id: u64) -> Self { + Binding::EncodedSid { + s_id, + t: None, + op: None, + } + } + + /// Create an `EncodedSid` binding with history metadata. + /// + /// Used by ref-valued object bindings emitted from the + /// late-materialised binary scan path in history mode. + pub fn encoded_sid_with_t_op(s_id: u64, t: i64, op: bool) -> Self { + Binding::EncodedSid { + s_id, + t: Some(t), + op: Some(op), + } + } + /// Create a binding from a raw IRI string. /// /// Used for graph source results where IRIs are generated from templates @@ -335,13 +463,13 @@ impl Binding { pub fn is_matchable(&self) -> bool { matches!( self, - Binding::Sid(_) | Binding::IriMatch { .. } | Binding::Iri(_) | Binding::Lit { .. } + Binding::Sid { .. } | Binding::IriMatch { .. } | Binding::Iri(_) | Binding::Lit { .. } ) } /// Check if this is a reference/Sid binding (not IriMatch) pub fn is_sid(&self) -> bool { - matches!(self, Binding::Sid(_)) + matches!(self, Binding::Sid { .. }) } /// Check if this is an IriMatch binding (multi-ledger IRI reference) @@ -353,7 +481,7 @@ impl Binding { pub fn is_iri_type(&self) -> bool { matches!( self, - Binding::Sid(_) + Binding::Sid { .. } | Binding::IriMatch { .. } | Binding::Iri(_) | Binding::EncodedSid { .. } @@ -394,7 +522,7 @@ impl Binding { /// Get the raw s_id from an EncodedSid binding. pub fn encoded_s_id(&self) -> Option { match self { - Binding::EncodedSid { s_id } => Some(*s_id), + Binding::EncodedSid { s_id, .. } => Some(*s_id), _ => None, } } @@ -418,7 +546,7 @@ impl Binding { "as_sid() called on EncodedSid — use GraphDbRef::eager() for infrastructure queries" ); match self { - Binding::Sid(sid) => Some(sid), + Binding::Sid { sid, .. } => Some(sid), _ => None, } } @@ -430,7 +558,7 @@ impl Binding { /// For others: returns None pub fn get_sid_for_ledger(&self, _ledger_alias: &str) -> Option<&Sid> { match self { - Binding::Sid(sid) => Some(sid), + Binding::Sid { sid, .. } => Some(sid), Binding::IriMatch { primary_sid, .. } => Some(primary_sid), _ => None, } @@ -502,22 +630,6 @@ impl Binding { } } - /// Get the operation type if this is a Lit binding with op set - pub fn op(&self) -> Option { - match self { - Binding::Lit { op, .. } => *op, - _ => None, - } - } - - /// Get the transaction time if this is a Lit binding with t set - pub fn t(&self) -> Option { - match self { - Binding::Lit { t, .. } => *t, - _ => None, - } - } - /// Check if this is a grouped binding (produced by GROUP BY) pub fn is_grouped(&self) -> bool { matches!(self, Binding::Grouped(_)) @@ -599,7 +711,7 @@ impl From<&Binding> for bool { } => *b, Binding::Lit { .. } => true, Binding::EncodedLit { .. } => true, - Binding::Sid(_) => true, + Binding::Sid { .. } => true, Binding::IriMatch { .. } => true, Binding::Iri(_) => true, Binding::EncodedSid { .. } => true, @@ -630,8 +742,12 @@ impl PartialEq for Binding { (Binding::Unbound, Binding::Unbound) => true, (Binding::Poisoned, Binding::Poisoned) => true, - // Same-variant SID comparison (single-ledger mode) - (Binding::Sid(a), Binding::Sid(b)) => a == b, + // Same-variant SID comparison (single-ledger mode). + // `t` and `op` are metadata only and intentionally ignored — same + // discipline as the `Lit` variant, so set semantics (joins, + // DISTINCT, GROUP BY) treat a metadata-bearing object binding as + // equal to a metadata-free one with the same SID. + (Binding::Sid { sid: a, .. }, Binding::Sid { sid: b, .. }) => a == b, // IriMatch: compare canonical IRIs (multi-ledger mode) // This is the key for correct cross-ledger joins @@ -650,12 +766,12 @@ impl PartialEq for Binding { // Sid vs IriMatch: These should not be compared directly. // If this happens, it indicates mixed single/multi-ledger mode which is a bug. // Return false to be conservative (no accidental matches). - (Binding::Sid(_), Binding::IriMatch { .. }) => false, - (Binding::IriMatch { .. }, Binding::Sid(_)) => false, + (Binding::Sid { .. }, Binding::IriMatch { .. }) => false, + (Binding::IriMatch { .. }, Binding::Sid { .. }) => false, // Sid vs Iri: Cannot compare without decode context - (Binding::Sid(_), Binding::Iri(_)) => false, - (Binding::Iri(_), Binding::Sid(_)) => false, + (Binding::Sid { .. }, Binding::Iri(_)) => false, + (Binding::Iri(_), Binding::Sid { .. }) => false, ( Binding::Lit { @@ -706,20 +822,24 @@ impl PartialEq for Binding { } }, - // EncodedSid: compare by s_id directly (single-ledger only) - (Binding::EncodedSid { s_id: a }, Binding::EncodedSid { s_id: b }) => a == b, + // EncodedSid: compare by s_id directly (single-ledger only). + // `t` / `op` are metadata only and ignored, mirroring `Sid`. + ( + Binding::EncodedSid { s_id: a, .. }, + Binding::EncodedSid { s_id: b, .. }, + ) => a == b, // EncodedPid: compare by p_id directly (single-ledger only) (Binding::EncodedPid { p_id: a }, Binding::EncodedPid { p_id: b }) => a == b, // EncodedSid vs Sid: NOT equal (don't mix encoded/decoded modes) // This prevents accidental mixing which could corrupt hash structures - (Binding::EncodedSid { .. }, Binding::Sid(_)) => false, - (Binding::Sid(_), Binding::EncodedSid { .. }) => false, + (Binding::EncodedSid { .. }, Binding::Sid { .. }) => false, + (Binding::Sid { .. }, Binding::EncodedSid { .. }) => false, // EncodedPid vs Sid: NOT equal - (Binding::EncodedPid { .. }, Binding::Sid(_)) => false, - (Binding::Sid(_), Binding::EncodedPid { .. }) => false, + (Binding::EncodedPid { .. }, Binding::Sid { .. }) => false, + (Binding::Sid { .. }, Binding::EncodedPid { .. }) => false, // EncodedSid/EncodedPid vs IriMatch/Iri: NOT equal (single vs multi-ledger) (Binding::EncodedSid { .. }, Binding::IriMatch { .. } | Binding::Iri(_)) => false, @@ -757,7 +877,9 @@ impl std::hash::Hash for Binding { Binding::Poisoned => { 1u8.hash(state); } - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { + // `t` / `op` are metadata only and excluded from hashing + // to keep equality and hash consistent — see `PartialEq`. 2u8.hash(state); sid.hash(state); } @@ -797,8 +919,9 @@ impl std::hash::Hash for Binding { p_id.hash(state); } } - Binding::EncodedSid { s_id } => { - // Distinct discriminant from Sid (2) - they are not interchangeable + Binding::EncodedSid { s_id, .. } => { + // Distinct discriminant from Sid (2) - they are not interchangeable. + // `t` / `op` are metadata only and excluded. 7u8.hash(state); s_id.hash(state); } @@ -1366,7 +1489,7 @@ mod tests { fn test_batch_new() { let schema: Arc<[VarId]> = Arc::from(vec![VarId(0), VarId(1)].into_boxed_slice()); let columns = vec![ - vec![Binding::Sid(test_sid()), Binding::Unbound], + vec![Binding::sid(test_sid()), Binding::Unbound], vec![ Binding::lit(FlakeValue::Long(1), xsd_long()), Binding::lit(FlakeValue::Long(2), xsd_long()), @@ -1407,8 +1530,8 @@ mod tests { let schema: Arc<[VarId]> = Arc::from(vec![VarId(0), VarId(1)].into_boxed_slice()); let columns = vec![ vec![ - Binding::Sid(Sid::new(1, "a")), - Binding::Sid(Sid::new(1, "b")), + Binding::sid(Sid::new(1, "a")), + Binding::sid(Sid::new(1, "b")), ], vec![ Binding::lit(FlakeValue::Long(10), xsd_long()), @@ -1420,7 +1543,7 @@ mod tests { // Get by VarId let b = batch.get(0, VarId(0)).unwrap(); - assert!(matches!(b, Binding::Sid(_))); + assert!(matches!(b, Binding::Sid { .. })); let b = batch.get(1, VarId(1)).unwrap(); let (val, _) = b.as_lit().unwrap(); @@ -1544,7 +1667,7 @@ mod tests { fn test_batch_from_parts_round_trips_normal_batch() { let schema: Arc<[VarId]> = Arc::from(vec![VarId(0), VarId(1)].into_boxed_slice()); let columns = vec![ - vec![Binding::Sid(test_sid()), Binding::Unbound], + vec![Binding::sid(test_sid()), Binding::Unbound], vec![ Binding::lit(FlakeValue::Long(1), xsd_long()), Binding::lit(FlakeValue::Long(2), xsd_long()), @@ -1635,7 +1758,7 @@ mod tests { // is_poisoned assert!(poisoned.is_poisoned()); assert!(!Binding::Unbound.is_poisoned()); - assert!(!Binding::Sid(test_sid()).is_poisoned()); + assert!(!Binding::sid(test_sid()).is_poisoned()); assert!(!Binding::lit(FlakeValue::Long(42), xsd_long()).is_poisoned()); } @@ -1648,7 +1771,7 @@ mod tests { assert!(!Binding::Unbound.is_matchable()); // Sid IS matchable - assert!(Binding::Sid(test_sid()).is_matchable()); + assert!(Binding::sid(test_sid()).is_matchable()); // Lit IS matchable assert!(Binding::lit(FlakeValue::Long(42), xsd_long()).is_matchable()); @@ -1672,7 +1795,7 @@ mod tests { assert_ne!(Binding::Poisoned, Binding::Unbound); // Poisoned != Sid - assert_ne!(Binding::Poisoned, Binding::Sid(test_sid())); + assert_ne!(Binding::Poisoned, Binding::sid(test_sid())); } #[test] @@ -1701,7 +1824,7 @@ mod tests { // is_grouped assert!(grouped.is_grouped()); assert!(!Binding::Unbound.is_grouped()); - assert!(!Binding::Sid(test_sid()).is_grouped()); + assert!(!Binding::sid(test_sid()).is_grouped()); // as_grouped let inner = grouped.as_grouped().unwrap(); @@ -1889,14 +2012,14 @@ mod tests { let sid_a = test_sid(); let sid_b = test_sid(); - let a = Binding::Sid(sid_a.clone()); - let b = Binding::Sid(sid_b.clone()); + let a = Binding::sid(sid_a.clone()); + let b = Binding::sid(sid_b.clone()); // Should use PartialEq which compares SIDs assert!(a.eq_for_join(&b)); // Different SIDs should not match - let c = Binding::Sid(Sid::new(999, "other")); + let c = Binding::sid(Sid::new(999, "other")); assert!(!a.eq_for_join(&c)); } @@ -1919,7 +2042,7 @@ mod tests { ledger_alias: Arc::from("test/ledger"), iri: Arc::from("http://example.org/test"), }; - let sid = Binding::Sid(test_sid()); + let sid = Binding::sid(test_sid()); assert!(!iri_match.eq_for_join(&sid)); } diff --git a/fluree-db-query/src/bm25/operator.rs b/fluree-db-query/src/bm25/operator.rs index c87999cc9..7f2e6e89c 100644 --- a/fluree-db-query/src/bm25/operator.rs +++ b/fluree-db-query/src/bm25/operator.rs @@ -271,7 +271,7 @@ impl Bm25SearchOperator { Ok(None) } } - Some(Binding::Sid(sid)) => { + Some(Binding::Sid { sid, .. }) => { // If user bound f:searchText to an IRI, treat its decoded IRI as the search string. // (Not typical, but keeps query robust.) Ok(ctx.decode_sid(sid)) @@ -286,7 +286,7 @@ impl Bm25SearchOperator { } Some(Binding::Grouped(_)) => Ok(None), // EncodedSid/EncodedPid: decode to IRI string if store available - Some(Binding::EncodedSid { s_id }) => { + Some(Binding::EncodedSid { s_id, .. }) => { // Novelty-aware: use graph_view() for subject resolution. match ctx.resolve_subject_iri(*s_id) { Some(Ok(iri)) => Ok(Some(iri)), diff --git a/fluree-db-query/src/dataset_operator.rs b/fluree-db-query/src/dataset_operator.rs index c77e414aa..4c7a38f49 100644 --- a/fluree-db-query/src/dataset_operator.rs +++ b/fluree-db-query/src/dataset_operator.rs @@ -231,7 +231,7 @@ fn stamp_binding( ctx: &ExecutionContext<'_>, ) -> Result { match binding { - Binding::Sid(ref sid) => sid_to_iri_match(sid, ledger_id, ctx), + Binding::Sid { ref sid, .. } => sid_to_iri_match(sid, ledger_id, ctx), Binding::EncodedSid { .. } | Binding::EncodedPid { .. } => Err(QueryError::Internal( "EncodedSid/EncodedPid reached stamp_provenance — binary store should have \ been disabled for multi-ledger datasets" diff --git a/fluree-db-query/src/expression/compare.rs b/fluree-db-query/src/expression/compare.rs index cc06c998c..7ed9b1b46 100644 --- a/fluree-db-query/src/expression/compare.rs +++ b/fluree-db-query/src/expression/compare.rs @@ -79,7 +79,7 @@ fn fast_eq_ne_for_iri_bindings( }; match binding { - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { let Some(lhs_iri) = ctx .resolve_subject_iri(*s_id) .transpose() @@ -100,7 +100,7 @@ fn fast_eq_ne_for_iri_bindings( log_fastpath_hit_once("EncodedSid"); Ok(Some(out)) } - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { let eq = match other { ComparableValue::Sid(rhs) => { if sid == &rhs { diff --git a/fluree-db-query/src/expression/eval.rs b/fluree-db-query/src/expression/eval.rs index 4734f983b..2d2115402 100644 --- a/fluree-db-query/src/expression/eval.rs +++ b/fluree-db-query/src/expression/eval.rs @@ -123,12 +123,12 @@ impl Expression { })?; Ok(ComparableValue::try_from(&val).ok()) } - Some(Binding::Sid(sid)) => Ok(Some(ComparableValue::Sid(sid.clone()))), + Some(Binding::Sid { sid, .. }) => Ok(Some(ComparableValue::Sid(sid.clone()))), Some(Binding::IriMatch { iri, .. }) => { Ok(Some(ComparableValue::Iri(Arc::clone(iri)))) } Some(Binding::Iri(iri)) => Ok(Some(ComparableValue::Iri(Arc::clone(iri)))), - Some(Binding::EncodedSid { s_id }) => { + Some(Binding::EncodedSid { s_id, .. }) => { let Some(resolved) = ctx.and_then(|c| c.resolve_subject_iri(*s_id)) else { return Ok(None); }; diff --git a/fluree-db-query/src/expression/fluree.rs b/fluree-db-query/src/expression/fluree.rs index e04c4247c..b78977351 100644 --- a/fluree-db-query/src/expression/fluree.rs +++ b/fluree-db-query/src/expression/fluree.rs @@ -1,11 +1,21 @@ //! Fluree-specific function implementations //! -//! Implements Fluree-specific functions: T (transaction time), OP (operation type) +//! Implements Fluree-specific functions: T (transaction time), OP (operation type). +//! +//! Both functions delegate to the central `Binding::t()` / `Binding::op()` +//! accessors so they handle every metadata-bearing variant uniformly: +//! `Lit`, `EncodedLit`, `Sid`, and `EncodedSid`. Adding a new variant +//! that carries history metadata only needs the accessor to learn about +//! it — these evaluators stay unchanged. +//! +//! `OP(?v)` returns a boolean (`true` = assert, `false` = retract) — +//! this matches the on-disk `Flake.op` representation and avoids a +//! per-row Arc allocation. Users compare with `true` / `false` rather +//! than `"assert"` / `"retract"` strings. -use crate::binding::{Binding, RowAccess}; +use crate::binding::RowAccess; use crate::error::Result; use crate::ir::Expression; -use std::sync::Arc; use super::helpers::check_arity; use super::value::ComparableValue; @@ -14,15 +24,8 @@ pub fn eval_t(args: &[Expression], row: &R) -> Result { - return Ok(Some(ComparableValue::Long(*t))); - } - // Late-materialized binary bindings still carry `t` directly. - Binding::EncodedLit { t, .. } => { - return Ok(Some(ComparableValue::Long(*t))); - } - _ => {} + if let Some(t) = binding.t() { + return Ok(Some(ComparableValue::Long(t))); } } } @@ -32,9 +35,10 @@ pub fn eval_t(args: &[Expression], row: &R) -> Result(args: &[Expression], row: &R) -> Result> { check_arity(args, 1, "OP")?; if let Expression::Var(var_id) = &args[0] { - if let Some(Binding::Lit { op: Some(op), .. }) = row.get(*var_id) { - let op_str = if *op { "assert" } else { "retract" }; - return Ok(Some(ComparableValue::String(Arc::from(op_str)))); + if let Some(binding) = row.get(*var_id) { + if let Some(op) = binding.op() { + return Ok(Some(ComparableValue::Bool(op))); + } } } Ok(None) diff --git a/fluree-db-query/src/expression/helpers.rs b/fluree-db-query/src/expression/helpers.rs index 251cc22cd..4dc0c6026 100644 --- a/fluree-db-query/src/expression/helpers.rs +++ b/fluree-db-query/src/expression/helpers.rs @@ -265,7 +265,7 @@ fn encoded_binding_cache_key(binding: &Binding) -> Option Some(EncodedBindingCacheKey::Sid { s_id: *s_id }), + Binding::EncodedSid { s_id, .. } => Some(EncodedBindingCacheKey::Sid { s_id: *s_id }), Binding::EncodedPid { p_id } => Some(EncodedBindingCacheKey::Pid { p_id: *p_id }), _ => None, } diff --git a/fluree-db-query/src/expression/rdf.rs b/fluree-db-query/src/expression/rdf.rs index 0c3bf3fed..43fa388b0 100644 --- a/fluree-db-query/src/expression/rdf.rs +++ b/fluree-db-query/src/expression/rdf.rs @@ -72,7 +72,7 @@ pub fn eval_datatype( })?; Ok(Some(format_datatype_sid(&dt_sid))) } - Binding::Sid(_) | Binding::IriMatch { .. } | Binding::Iri(_) => { + Binding::Sid { sid: _, .. } | Binding::IriMatch { .. } | Binding::Iri(_) => { Ok(Some(ComparableValue::String(Arc::from("@id")))) } Binding::Unbound | Binding::Poisoned => Ok(None), @@ -158,10 +158,10 @@ fn fast_same_term_encoded_ids( }; match binding { - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { // If both sides are vars and both are EncodedSid, compare directly. if let Expression::Var(v2) = other_expr { - if let Some(Binding::EncodedSid { s_id: s2 }) = row.get(*v2) { + if let Some(Binding::EncodedSid { s_id: s2, .. }) = row.get(*v2) { return Ok(Some(*s_id == *s2)); } } @@ -322,7 +322,7 @@ mod tests { // IRI() of a Sid should return the Sid unchanged let schema: Arc<[VarId]> = Arc::from(vec![VarId(0)].into_boxed_slice()); let sid = Sid::new(100, "x"); - let col = vec![Binding::Sid(sid.clone())]; + let col = vec![Binding::sid(sid.clone())]; let batch = Batch::new(schema, vec![col]).unwrap(); let row = batch.row_view(0).unwrap(); diff --git a/fluree-db-query/src/expression/types.rs b/fluree-db-query/src/expression/types.rs index c840b4445..1a51d5785 100644 --- a/fluree-db-query/src/expression/types.rs +++ b/fluree-db-query/src/expression/types.rs @@ -78,7 +78,7 @@ pub fn eval_is_blank( match &args[0] { Expression::Var(v) => { let is_blank = match row.get(*v) { - Some(Binding::Sid(s)) => s.namespace_code == namespaces::BLANK_NODE, + Some(Binding::Sid { sid: s, .. }) => s.namespace_code == namespaces::BLANK_NODE, Some(Binding::IriMatch { iri, primary_sid, .. }) => { @@ -86,7 +86,7 @@ pub fn eval_is_blank( || iri.as_ref().starts_with("_:") } Some(Binding::Iri(iri)) => iri.as_ref().starts_with("_:"), - Some(Binding::EncodedSid { s_id }) => { + Some(Binding::EncodedSid { s_id, .. }) => { SubjectId::from_u64(*s_id).ns_code() == namespaces::BLANK_NODE } _ => false, diff --git a/fluree-db-query/src/expression/value.rs b/fluree-db-query/src/expression/value.rs index d43118162..68767bfa1 100644 --- a/fluree-db-query/src/expression/value.rs +++ b/fluree-db-query/src/expression/value.rs @@ -374,7 +374,7 @@ impl ComparableValue { FlakeValue::Boolean(b), datatypes.xsd_boolean.clone(), )), - ComparableValue::Sid(sid) => Ok(Binding::Sid(sid)), + ComparableValue::Sid(sid) => Ok(Binding::sid(sid)), ComparableValue::Vector(v) => Ok(Binding::lit( FlakeValue::Vector(v.to_vec()), datatypes.fluree_vector.clone(), @@ -409,7 +409,7 @@ impl ComparableValue { // (UUID, IRI() function) that don't exist in the database. if let Some(ctx) = ctx { if let Some(sid) = ctx.active_snapshot.encode_iri_strict(&iri) { - return Ok(Binding::Sid(sid)); + return Ok(Binding::sid(sid)); } } Ok(Binding::Iri(iri)) diff --git a/fluree-db-query/src/fast_group_count_firsts.rs b/fluree-db-query/src/fast_group_count_firsts.rs index 9a021bea4..3aa565365 100644 --- a/fluree-db-query/src/fast_group_count_firsts.rs +++ b/fluree-db-query/src/fast_group_count_firsts.rs @@ -253,7 +253,7 @@ impl Operator for PredicateGroupCountFirstsOperator { self.pos += 1; if o_type == OType::IRI_REF.as_u16() { - col_o.push(Binding::EncodedSid { s_id: o_key }); + col_o.push(Binding::encoded_sid(o_key)); } else { let val = view .decode_value(o_type, o_key, p_id) @@ -1428,7 +1428,7 @@ fn compute_group_by_object_star_topk( for (k, st) in rows { if k.o_type == OType::IRI_REF.as_u16() { - col_o1.push(Binding::EncodedSid { s_id: k.o_key }); + col_o1.push(Binding::encoded_sid(k.o_key)); } else { let val = view .decode_value(k.o_type, k.o_key, p_id) @@ -1453,22 +1453,13 @@ fn compute_group_by_object_star_topk( dt_count.clone(), )); if want_min { - col_min.push( - st.min_s - .map_or(Binding::Unbound, |s| Binding::EncodedSid { s_id: s }), - ); + col_min.push(st.min_s.map_or(Binding::Unbound, Binding::encoded_sid)); } if want_max { - col_max.push( - st.max_s - .map_or(Binding::Unbound, |s| Binding::EncodedSid { s_id: s }), - ); + col_max.push(st.max_s.map_or(Binding::Unbound, Binding::encoded_sid)); } if want_sample { - col_sample.push( - st.sample_s - .map_or(Binding::Unbound, |s| Binding::EncodedSid { s_id: s }), - ); + col_sample.push(st.sample_s.map_or(Binding::Unbound, Binding::encoded_sid)); } } diff --git a/fluree-db-query/src/fast_label_regex_type.rs b/fluree-db-query/src/fast_label_regex_type.rs index e00611598..1184bc4c1 100644 --- a/fluree-db-query/src/fast_label_regex_type.rs +++ b/fluree-db-query/src/fast_label_regex_type.rs @@ -208,7 +208,7 @@ pub fn label_regex_type_operator( if !has { continue; } - col_s.push(Binding::EncodedSid { s_id }); + col_s.push(Binding::encoded_sid(s_id)); let (label, lang) = &hit_labels[idx]; let lit = FlakeValue::String(label.clone()); col_label.push(match lang { diff --git a/fluree-db-query/src/fast_star_const_order_topk.rs b/fluree-db-query/src/fast_star_const_order_topk.rs index a408b6805..f5acb5eb9 100644 --- a/fluree-db-query/src/fast_star_const_order_topk.rs +++ b/fluree-db-query/src/fast_star_const_order_topk.rs @@ -131,7 +131,7 @@ pub fn star_const_ordered_limit_operator( let mut col_s: Vec = Vec::with_capacity(rows.len()); let mut col_label: Vec = Vec::with_capacity(rows.len()); for (label, lang, s_id) in rows { - col_s.push(Binding::EncodedSid { s_id }); + col_s.push(Binding::encoded_sid(s_id)); let lit = FlakeValue::String(label.to_string()); col_label.push(match lang { Some(tag) => Binding::lit_lang(lit, tag), diff --git a/fluree-db-query/src/filter.rs b/fluree-db-query/src/filter.rs index bb5cd8d9f..50c11c88f 100644 --- a/fluree-db-query/src/filter.rs +++ b/fluree-db-query/src/filter.rs @@ -341,7 +341,7 @@ fn try_eval_simple_exists_semijoin( let Some(binding) = batch.get(row_idx, *subject_var) else { return Ok(Some(false)); }; - let Binding::Sid(sid) = binding else { + let Binding::Sid { sid, .. } = binding else { // Only handle the common single-ledger SID binding here. return Ok(None); }; diff --git a/fluree-db-query/src/geo_search.rs b/fluree-db-query/src/geo_search.rs index b99186384..dbff2615b 100644 --- a/fluree-db-query/src/geo_search.rs +++ b/fluree-db-query/src/geo_search.rs @@ -299,7 +299,7 @@ impl GeoSearchOperator { } }; let subject_pos = *self.out_pos.get(&self.pattern.subject_var).unwrap(); - row[subject_pos] = Binding::Sid(subject_sid); + row[subject_pos] = Binding::sid(subject_sid); // Add distance binding if requested if let Some(dist_var) = self.pattern.distance_var { diff --git a/fluree-db-query/src/group_aggregate.rs b/fluree-db-query/src/group_aggregate.rs index 9cab8d6c5..addd59295 100644 --- a/fluree-db-query/src/group_aggregate.rs +++ b/fluree-db-query/src/group_aggregate.rs @@ -106,7 +106,8 @@ fn compare_for_minmax( let store = gv.store(); // Fast path 1: subject IDs (IRIs) — compare lexicographically without allocation. - if let (Binding::EncodedSid { s_id: a_id }, Binding::EncodedSid { s_id: b_id }) = (a, b) { + if let (Binding::EncodedSid { s_id: a_id, .. }, Binding::EncodedSid { s_id: b_id, .. }) = (a, b) + { if let Ok(ord) = store.compare_subject_iri_lex(*a_id, *b_id) { return ord; } @@ -170,7 +171,7 @@ fn materialize_for_minmax(binding: &Binding, gv: Option<&BinaryGraphView>) -> Bi } => { // BinaryGraphView handles novelty watermark routing internally. match gv.decode_value_from_kind(*o_kind, *o_key, *p_id, *dt_id, *lang_id) { - Ok(fluree_db_core::FlakeValue::Ref(sid)) => Binding::Sid(sid), + Ok(fluree_db_core::FlakeValue::Ref(sid)) => Binding::sid(sid), Ok(val) => { let dt_sid = store .dt_sids() @@ -194,12 +195,12 @@ fn materialize_for_minmax(binding: &Binding, gv: Option<&BinaryGraphView>) -> Bi Err(_) => binding.clone(), } } - Binding::EncodedSid { s_id } => match gv.resolve_subject_sid(*s_id) { - Ok(sid) => Binding::Sid(sid), + Binding::EncodedSid { s_id, .. } => match gv.resolve_subject_sid(*s_id) { + Ok(sid) => Binding::sid(sid), Err(_) => binding.clone(), }, Binding::EncodedPid { p_id } => match store.resolve_predicate_iri(*p_id) { - Some(iri) => Binding::Sid(store.encode_iri(iri)), + Some(iri) => Binding::sid(store.encode_iri(iri)), None => binding.clone(), }, _ => binding.clone(), @@ -423,7 +424,7 @@ impl Hash for MaterializedLitKey { /// Also used by SemijoinOperator for EXISTS hash probing. pub(crate) fn binding_to_group_key_owned(binding: &Binding) -> GroupKeyOwned { match binding { - Binding::EncodedSid { s_id } => GroupKeyOwned::Sid(*s_id), + Binding::EncodedSid { s_id, .. } => GroupKeyOwned::Sid(*s_id), Binding::EncodedPid { p_id } => GroupKeyOwned::Pid(*p_id), Binding::EncodedLit { o_kind, @@ -443,7 +444,9 @@ pub(crate) fn binding_to_group_key_owned(binding: &Binding) -> GroupKeyOwned { dt_id: *dt_id, lang_id: *lang_id, }, - Binding::Sid(sid) => GroupKeyOwned::MaterializedSid(sid.namespace_code, sid.name.clone()), + Binding::Sid { sid, .. } => { + GroupKeyOwned::MaterializedSid(sid.namespace_code, sid.name.clone()) + } Binding::Lit { val, dtc, .. } => { GroupKeyOwned::MaterializedLit(flake_value_to_key(val, dtc)) } @@ -1013,25 +1016,25 @@ mod tests { let columns = vec![ // ?venue vec![ - Binding::Sid(Sid::new(100, "venueA")), - Binding::Sid(Sid::new(100, "venueA")), - Binding::Sid(Sid::new(100, "venueA")), - Binding::Sid(Sid::new(100, "venueA")), - Binding::Sid(Sid::new(100, "venueA")), - Binding::Sid(Sid::new(100, "venueB")), - Binding::Sid(Sid::new(100, "venueB")), - Binding::Sid(Sid::new(100, "venueB")), + Binding::sid(Sid::new(100, "venueA")), + Binding::sid(Sid::new(100, "venueA")), + Binding::sid(Sid::new(100, "venueA")), + Binding::sid(Sid::new(100, "venueA")), + Binding::sid(Sid::new(100, "venueA")), + Binding::sid(Sid::new(100, "venueB")), + Binding::sid(Sid::new(100, "venueB")), + Binding::sid(Sid::new(100, "venueB")), ], // ?paper vec![ - Binding::Sid(Sid::new(200, "paper1")), - Binding::Sid(Sid::new(200, "paper2")), - Binding::Sid(Sid::new(200, "paper3")), - Binding::Sid(Sid::new(200, "paper4")), - Binding::Sid(Sid::new(200, "paper5")), - Binding::Sid(Sid::new(200, "paper6")), - Binding::Sid(Sid::new(200, "paper7")), - Binding::Sid(Sid::new(200, "paper8")), + Binding::sid(Sid::new(200, "paper1")), + Binding::sid(Sid::new(200, "paper2")), + Binding::sid(Sid::new(200, "paper3")), + Binding::sid(Sid::new(200, "paper4")), + Binding::sid(Sid::new(200, "paper5")), + Binding::sid(Sid::new(200, "paper6")), + Binding::sid(Sid::new(200, "paper7")), + Binding::sid(Sid::new(200, "paper8")), ], ]; let batch = Batch::new(schema.clone(), columns).unwrap(); @@ -1093,7 +1096,7 @@ mod tests { let venue_a_count = results .iter() .find(|(v, _)| { - if let Binding::Sid(sid) = v { + if let Binding::Sid { sid, .. } = v { sid.name.as_ref() == "venueA" } else { false @@ -1103,7 +1106,7 @@ mod tests { let venue_b_count = results .iter() .find(|(v, _)| { - if let Binding::Sid(sid) = v { + if let Binding::Sid { sid, .. } = v { sid.name.as_ref() == "venueB" } else { false @@ -1131,11 +1134,11 @@ mod tests { let columns = vec![ // ?category vec![ - Binding::Sid(Sid::new(100, "catA")), - Binding::Sid(Sid::new(100, "catA")), - Binding::Sid(Sid::new(100, "catA")), - Binding::Sid(Sid::new(100, "catB")), - Binding::Sid(Sid::new(100, "catB")), + Binding::sid(Sid::new(100, "catA")), + Binding::sid(Sid::new(100, "catA")), + Binding::sid(Sid::new(100, "catA")), + Binding::sid(Sid::new(100, "catB")), + Binding::sid(Sid::new(100, "catB")), ], // ?value vec![ @@ -1197,7 +1200,7 @@ mod tests { let sum_val = batch.get_by_col(row_idx, 1); let avg_val = batch.get_by_col(row_idx, 2); - if let Binding::Sid(sid) = cat { + if let Binding::Sid { sid, .. } = cat { let sum = match sum_val { Binding::Lit { val: FlakeValue::Long(n), diff --git a/fluree-db-query/src/groupby.rs b/fluree-db-query/src/groupby.rs index c294c034e..016a32213 100644 --- a/fluree-db-query/src/groupby.rs +++ b/fluree-db-query/src/groupby.rs @@ -332,7 +332,7 @@ mod tests { let schema: Arc<[VarId]> = Arc::from(vec![VarId(0), VarId(1), VarId(2)].into_boxed_slice()); let columns = vec![ vec![Binding::lit(FlakeValue::String("NYC".into()), xsd_string())], - vec![Binding::Sid(Sid::new(100, "alice"))], + vec![Binding::sid(Sid::new(100, "alice"))], vec![Binding::lit(FlakeValue::Long(30), xsd_long())], ]; let batch = Batch::new(schema.clone(), columns).unwrap(); @@ -373,7 +373,7 @@ mod tests { let schema: Arc<[VarId]> = Arc::from(vec![VarId(0), VarId(1), VarId(2)].into_boxed_slice()); let columns = vec![ vec![Binding::lit(FlakeValue::String("NYC".into()), xsd_string())], - vec![Binding::Sid(Sid::new(100, "alice"))], + vec![Binding::sid(Sid::new(100, "alice"))], vec![Binding::lit(FlakeValue::Long(30), xsd_long())], ]; let batch = Batch::new(schema.clone(), columns).unwrap(); @@ -383,7 +383,7 @@ mod tests { let row = vec![ Binding::lit(FlakeValue::String("NYC".into()), xsd_string()), - Binding::Sid(Sid::new(100, "alice")), + Binding::sid(Sid::new(100, "alice")), Binding::lit(FlakeValue::Long(30), xsd_long()), ]; @@ -413,9 +413,9 @@ mod tests { Binding::lit(FlakeValue::String("NYC".into()), xsd_string()), ], vec![ - Binding::Sid(Sid::new(100, "alice")), - Binding::Sid(Sid::new(100, "bob")), - Binding::Sid(Sid::new(100, "carol")), + Binding::sid(Sid::new(100, "alice")), + Binding::sid(Sid::new(100, "bob")), + Binding::sid(Sid::new(100, "carol")), ], vec![ Binding::lit(FlakeValue::Long(30), xsd_long()), @@ -501,10 +501,10 @@ mod tests { Binding::lit(FlakeValue::String("LA".into()), xsd_string()), ], vec![ - Binding::Sid(Sid::new(100, "alice")), - Binding::Sid(Sid::new(100, "bob")), - Binding::Sid(Sid::new(100, "carol")), - Binding::Sid(Sid::new(100, "dan")), + Binding::sid(Sid::new(100, "alice")), + Binding::sid(Sid::new(100, "bob")), + Binding::sid(Sid::new(100, "carol")), + Binding::sid(Sid::new(100, "dan")), ], vec![ Binding::lit(FlakeValue::Long(30), xsd_long()), diff --git a/fluree-db-query/src/ir.rs b/fluree-db-query/src/ir.rs index 817a62448..8e5fcb22e 100644 --- a/fluree-db-query/src/ir.rs +++ b/fluree-db-query/src/ir.rs @@ -1963,9 +1963,11 @@ pub enum Function { // ========================================================================= // Fluree-specific functions // ========================================================================= - /// Transaction time + /// Transaction time of the matching flake (i64). T, - /// Operation type for history queries ("assert" or "retract") + /// Operation type of the matching flake in history queries — boolean + /// (`true` = assert, `false` = retract). Mirrors `Flake.op` on disk; + /// returns `None` for current-state scans. Op, // ========================================================================= diff --git a/fluree-db-query/src/join.rs b/fluree-db-query/src/join.rs index a3dfafc9d..e68f81aa8 100644 --- a/fluree-db-query/src/join.rs +++ b/fluree-db-query/src/join.rs @@ -648,7 +648,7 @@ impl NestedLoopJoinOperator { !matches!( binding, Binding::Unbound - | Binding::Sid(_) + | Binding::Sid { .. } | Binding::IriMatch { .. } | Binding::Iri(_) | Binding::EncodedSid { .. } @@ -678,14 +678,14 @@ impl NestedLoopJoinOperator { match instr.position { PatternPosition::Subject => { match binding { - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { pattern.s = Ref::Sid(sid.clone()); } Binding::IriMatch { iri, .. } | Binding::Iri(iri) => { // Use Ref::Iri so scan can encode for each target ledger pattern.s = Ref::Iri(iri.clone()); } - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { // Resolve encoded s_id to IRI (novelty-aware via BinaryGraphView) if let Some(gv) = gv { let iri = gv.resolve_subject_iri(*s_id).map_err(|e| { @@ -718,14 +718,14 @@ impl NestedLoopJoinOperator { } PatternPosition::Predicate => { match binding { - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { pattern.p = Ref::Sid(sid.clone()); } Binding::IriMatch { iri, .. } | Binding::Iri(iri) => { // Use Term::Iri so scan can encode for each target ledger pattern.p = Ref::Iri(iri.clone()); } - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { // Allow cross-position reuse: an IRI bound as a subject/object can // be used to bind a predicate position. Resolve via subject dict. if let Some(gv) = gv { @@ -759,7 +759,7 @@ impl NestedLoopJoinOperator { } PatternPosition::Object => { match binding { - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { pattern.o = Term::Sid(sid.clone()); } Binding::IriMatch { iri, .. } | Binding::Iri(iri) => { @@ -802,7 +802,7 @@ impl NestedLoopJoinOperator { } // Otherwise leave as variable } - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { // Resolve encoded s_id to IRI (novelty-aware) if let Some(gv) = gv { let iri = gv.resolve_subject_iri(*s_id).map_err(|e| { @@ -1129,8 +1129,8 @@ impl Operator for NestedLoopJoinOperator { let left_batch = self.current_left_batch.as_ref().unwrap(); let store = ctx.binary_store.as_deref(); match left_batch.get_by_col(left_row, left_col) { - Binding::EncodedSid { s_id } => Some(*s_id), - Binding::Sid(sid) => store.and_then(|s| { + Binding::EncodedSid { s_id, .. } => Some(*s_id), + Binding::Sid { sid, .. } => store.and_then(|s| { s.find_subject_id_by_parts(sid.namespace_code, &sid.name) .ok() .flatten() @@ -1602,7 +1602,7 @@ impl NestedLoopJoinOperator { let obj_binding = if o_type_val == OType::IRI_REF.as_u16() || o_type_val == OType::BLANK_NODE.as_u16() { - Binding::EncodedSid { s_id: o_key_val } + Binding::encoded_sid(o_key_val) } else { let p_id = entry.p_const.unwrap_or_else(|| batch.p_id.get_or(row, 0)); let o_i = batch.o_i.get_or(row, u32::MAX); @@ -1738,7 +1738,14 @@ impl NestedLoopJoinOperator { )) })?, }; - materialized_object_binding(store, o_type_val, p_id, val, Some(t)) + materialized_object_binding( + store, + o_type_val, + p_id, + val, + Some(t), + None, + ) } } }; @@ -2140,7 +2147,7 @@ impl NestedLoopJoinOperator { })?; let mut right_bindings = Vec::with_capacity(self.right_new_vars.len()); for _ in &self.right_new_vars { - right_bindings.push(Binding::EncodedSid { s_id }); + right_bindings.push(Binding::encoded_sid(s_id)); } if !self.apply_right_scan_inline_ops(ctx, &mut right_bindings)? { continue; @@ -2233,7 +2240,7 @@ impl NestedLoopJoinOperator { })?; let mut right_bindings = Vec::with_capacity(self.right_new_vars.len()); for _ in &self.right_new_vars { - right_bindings.push(Binding::EncodedSid { s_id }); + right_bindings.push(Binding::encoded_sid(s_id)); } if !self.apply_right_scan_inline_ops(ctx, &mut right_bindings)? { continue; @@ -2390,7 +2397,9 @@ fn build_probe_object_binding( ) -> Result { use fluree_db_core::o_type::{DecodeKind, OType}; - if let Some(binding) = late_materialized_object_binding(o_type_val, o_key_val, p_id, t, o_i) { + if let Some(binding) = + late_materialized_object_binding(o_type_val, o_key_val, p_id, t, o_i, None) + { return Ok(binding); } @@ -2424,6 +2433,7 @@ fn build_probe_object_binding( p_id, val, Some(t), + None, )) } @@ -2974,7 +2984,7 @@ mod tests { // Create a batch with one row that has NO Poisoned bindings let columns_normal = vec![ - vec![Binding::Sid(Sid::new(1, "alice"))], + vec![Binding::sid(Sid::new(1, "alice"))], vec![Binding::lit( FlakeValue::String("Alice".to_string()), Sid::new(2, "string"), @@ -2987,7 +2997,7 @@ mod tests { // Create a batch where Poisoned is in position 1 (NOT used for binding) let columns_poisoned_unused = vec![ - vec![Binding::Sid(Sid::new(1, "alice"))], + vec![Binding::sid(Sid::new(1, "alice"))], vec![Binding::Poisoned], // This is in position 1, not used for binding ?s ]; let batch_poisoned_unused = Batch::new(left_schema, columns_poisoned_unused).unwrap(); @@ -3292,7 +3302,7 @@ mod tests { // Left batch: ?s = some:subject (a Sid) let left_batch = Batch::new( left_schema, - vec![vec![Binding::Sid(Sid::new(1, "some:subject"))]], + vec![vec![Binding::sid(Sid::new(1, "some:subject"))]], ) .unwrap(); @@ -3301,7 +3311,7 @@ mod tests { let right_batch = Batch::new( right_schema, vec![ - vec![Binding::Sid(Sid::new(1, "some:other"))], + vec![Binding::sid(Sid::new(1, "some:other"))], vec![Binding::lit(FlakeValue::Long(42), Sid::new(2, "long"))], ], ) diff --git a/fluree-db-query/src/materializer.rs b/fluree-db-query/src/materializer.rs index 8cf0cb98b..4dfc31a3b 100644 --- a/fluree-db-query/src/materializer.rs +++ b/fluree-db-query/src/materializer.rs @@ -321,7 +321,7 @@ impl Materializer { match binding { Binding::Unbound | Binding::Poisoned => JoinKey::Absent, - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { match self.mode { JoinKeyMode::SingleLedger => { // In single-ledger mode, (namespace_code, name) is a valid key @@ -351,7 +351,7 @@ impl Materializer { Binding::Iri(iri) => JoinKey::Iri(Cow::Borrowed(iri.as_ref())), - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { match self.mode { JoinKeyMode::SingleLedger => JoinKey::Sid(*s_id), JoinKeyMode::MultiLedger => { @@ -422,13 +422,13 @@ impl Materializer { match binding { Binding::Unbound | Binding::Poisoned => None, - Binding::Sid(sid) => Some(ComparableValue::Sid(sid.clone())), + Binding::Sid { sid, .. } => Some(ComparableValue::Sid(sid.clone())), Binding::IriMatch { iri, .. } => Some(ComparableValue::Iri(Arc::clone(iri))), Binding::Iri(iri) => Some(ComparableValue::Iri(Arc::clone(iri))), - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { let iri = self.resolve_iri(*s_id); Some(ComparableValue::Iri(iri)) } @@ -472,7 +472,7 @@ impl Materializer { match binding { Binding::Unbound | Binding::Poisoned => None, - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { // Decode to full IRI string. // IMPORTANT: `namespace_code:name` is an internal representation and is not a full IRI. // Unknown namespace code → None (strict decode). @@ -494,7 +494,7 @@ impl Materializer { Binding::Iri(iri) => Some(Arc::clone(iri)), - Binding::EncodedSid { s_id } => Some(self.resolve_iri(*s_id)), + Binding::EncodedSid { s_id, .. } => Some(self.resolve_iri(*s_id)), Binding::EncodedPid { p_id } => self .graph_view @@ -537,20 +537,20 @@ impl Materializer { // Already materialized Binding::Unbound | Binding::Poisoned - | Binding::Sid(_) + | Binding::Sid { .. } | Binding::IriMatch { .. } | Binding::Iri(_) | Binding::Lit { .. } | Binding::Grouped(_) => binding.clone(), - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { let sid = self.resolve_sid(*s_id); - Binding::Sid(sid) + Binding::sid(sid) } Binding::EncodedPid { p_id } => { let sid = self.resolve_pid(*p_id); - Binding::Sid(sid) + Binding::sid(sid) } Binding::EncodedLit { @@ -565,7 +565,7 @@ impl Materializer { .graph_view .decode_value_from_kind(*o_kind, *o_key, *p_id, *dt_id, *lang_id) { - Ok(FlakeValue::Ref(sid)) => Binding::Sid(sid), + Ok(FlakeValue::Ref(sid)) => Binding::sid(sid), Ok(val) => { let dt_sid = self .graph_view diff --git a/fluree-db-query/src/minus.rs b/fluree-db-query/src/minus.rs index 19fd557e2..8cc56be92 100644 --- a/fluree-db-query/src/minus.rs +++ b/fluree-db-query/src/minus.rs @@ -545,16 +545,16 @@ mod tests { fn rows_match_both_bound_equal() { let shared = vec![VarId(0)]; let sid = Sid::new(100, "x"); - let input = batch_1row(&[VarId(0)], vec![Binding::Sid(sid.clone())]); - let minus = batch_1row(&[VarId(0)], vec![Binding::Sid(sid)]); + let input = batch_1row(&[VarId(0)], vec![Binding::sid(sid.clone())]); + let minus = batch_1row(&[VarId(0)], vec![Binding::sid(sid)]); assert!(rows_match(&shared, &input, 0, &minus, 0)); } #[test] fn rows_match_both_bound_unequal() { let shared = vec![VarId(0)]; - let input = batch_1row(&[VarId(0)], vec![Binding::Sid(Sid::new(100, "x"))]); - let minus = batch_1row(&[VarId(0)], vec![Binding::Sid(Sid::new(200, "y"))]); + let input = batch_1row(&[VarId(0)], vec![Binding::sid(Sid::new(100, "x"))]); + let minus = batch_1row(&[VarId(0)], vec![Binding::sid(Sid::new(200, "y"))]); assert!(!rows_match(&shared, &input, 0, &minus, 0)); } @@ -564,7 +564,7 @@ mod tests { // but no shared bound variables → match should NOT fire let shared = vec![VarId(0)]; let input = batch_1row(&[VarId(0)], vec![Binding::Unbound]); - let minus = batch_1row(&[VarId(0)], vec![Binding::Sid(Sid::new(100, "x"))]); + let minus = batch_1row(&[VarId(0)], vec![Binding::sid(Sid::new(100, "x"))]); assert!( !rows_match(&shared, &input, 0, &minus, 0), "no shared bound var → match must not fire" @@ -576,7 +576,7 @@ mod tests { // MINUS has Unbound, input has a value — trivially compatible // but no shared bound variables → match should NOT fire let shared = vec![VarId(0)]; - let input = batch_1row(&[VarId(0)], vec![Binding::Sid(Sid::new(100, "x"))]); + let input = batch_1row(&[VarId(0)], vec![Binding::sid(Sid::new(100, "x"))]); let minus = batch_1row(&[VarId(0)], vec![Binding::Unbound]); assert!( !rows_match(&shared, &input, 0, &minus, 0), @@ -600,7 +600,7 @@ mod tests { // Poisoned (from failed OPTIONAL) is not in domain let shared = vec![VarId(0)]; let input = batch_1row(&[VarId(0)], vec![Binding::Poisoned]); - let minus = batch_1row(&[VarId(0)], vec![Binding::Sid(Sid::new(100, "x"))]); + let minus = batch_1row(&[VarId(0)], vec![Binding::sid(Sid::new(100, "x"))]); assert!( !rows_match(&shared, &input, 0, &minus, 0), "poisoned is not matchable → no shared bound var" @@ -615,11 +615,11 @@ mod tests { let sid = Sid::new(100, "x"); let input = batch_1row( &[VarId(0), VarId(1)], - vec![Binding::Sid(sid.clone()), Binding::Unbound], + vec![Binding::sid(sid.clone()), Binding::Unbound], ); let minus = batch_1row( &[VarId(0), VarId(1)], - vec![Binding::Sid(sid), Binding::Sid(Sid::new(200, "y"))], + vec![Binding::sid(sid), Binding::sid(Sid::new(200, "y"))], ); assert!( rows_match(&shared, &input, 0, &minus, 0), @@ -634,11 +634,11 @@ mod tests { let sid = Sid::new(100, "x"); let input = batch_1row( &[VarId(0), VarId(1)], - vec![Binding::Sid(sid.clone()), Binding::Sid(Sid::new(300, "a"))], + vec![Binding::sid(sid.clone()), Binding::sid(Sid::new(300, "a"))], ); let minus = batch_1row( &[VarId(0), VarId(1)], - vec![Binding::Sid(sid), Binding::Sid(Sid::new(400, "b"))], + vec![Binding::sid(sid), Binding::sid(Sid::new(400, "b"))], ); assert!( !rows_match(&shared, &input, 0, &minus, 0), @@ -654,8 +654,8 @@ mod tests { let batch = batch_1row( &[VarId(0), VarId(1)], vec![ - Binding::Sid(Sid::new(100, "x")), - Binding::Sid(Sid::new(200, "y")), + Binding::sid(Sid::new(100, "x")), + Binding::sid(Sid::new(200, "y")), ], ); op.build_hash_index(vec![batch]); @@ -668,7 +668,7 @@ mod tests { let mut op = make_minus_with_shared(vec![VarId(0), VarId(1)]); let batch = batch_1row( &[VarId(0), VarId(1)], - vec![Binding::Sid(Sid::new(100, "x")), Binding::Unbound], + vec![Binding::sid(Sid::new(100, "x")), Binding::Unbound], ); op.build_hash_index(vec![batch]); assert!(op.minus_hash.is_empty()); @@ -679,20 +679,20 @@ mod tests { fn input_row_eliminated_hash_hit() { let mut op = make_minus_with_shared(vec![VarId(0)]); let sid = Sid::new(100, "x"); - let minus_batch = batch_1row(&[VarId(0)], vec![Binding::Sid(sid.clone())]); + let minus_batch = batch_1row(&[VarId(0)], vec![Binding::sid(sid.clone())]); op.build_hash_index(vec![minus_batch]); - let input = batch_1row(&[VarId(0)], vec![Binding::Sid(sid)]); + let input = batch_1row(&[VarId(0)], vec![Binding::sid(sid)]); assert!(op.input_row_eliminated(&input, 0)); } #[test] fn input_row_eliminated_hash_miss() { let mut op = make_minus_with_shared(vec![VarId(0)]); - let minus_batch = batch_1row(&[VarId(0)], vec![Binding::Sid(Sid::new(100, "x"))]); + let minus_batch = batch_1row(&[VarId(0)], vec![Binding::sid(Sid::new(100, "x"))]); op.build_hash_index(vec![minus_batch]); - let input = batch_1row(&[VarId(0)], vec![Binding::Sid(Sid::new(200, "y"))]); + let input = batch_1row(&[VarId(0)], vec![Binding::sid(Sid::new(200, "y"))]); assert!(!op.input_row_eliminated(&input, 0)); } @@ -705,13 +705,13 @@ mod tests { let sid = Sid::new(100, "x"); let minus_batch = batch_1row( &[VarId(0), VarId(1)], - vec![Binding::Sid(sid.clone()), Binding::Unbound], + vec![Binding::sid(sid.clone()), Binding::Unbound], ); op.build_hash_index(vec![minus_batch]); let input = batch_1row( &[VarId(0), VarId(1)], - vec![Binding::Sid(sid), Binding::Sid(Sid::new(200, "y"))], + vec![Binding::sid(sid), Binding::sid(Sid::new(200, "y"))], ); assert!( op.input_row_eliminated(&input, 0), @@ -728,13 +728,13 @@ mod tests { let sid = Sid::new(100, "x"); let minus_batch = batch_1row( &[VarId(0), VarId(1)], - vec![Binding::Sid(sid.clone()), Binding::Sid(Sid::new(200, "y"))], + vec![Binding::sid(sid.clone()), Binding::sid(Sid::new(200, "y"))], ); op.build_hash_index(vec![minus_batch]); let input = batch_1row( &[VarId(0), VarId(1)], - vec![Binding::Sid(sid), Binding::Unbound], + vec![Binding::sid(sid), Binding::Unbound], ); assert!( op.input_row_eliminated(&input, 0), diff --git a/fluree-db-query/src/object_binding.rs b/fluree-db-query/src/object_binding.rs index 61709e1fe..1115cda6a 100644 --- a/fluree-db-query/src/object_binding.rs +++ b/fluree-db-query/src/object_binding.rs @@ -14,17 +14,32 @@ fn encoded_i_val(o_i: u32) -> i32 { } } +/// Build a late-materialized object binding for the binary scan path. +/// +/// `op` is `Some(true|false)` only in history mode (assert/retract) — it +/// then flows onto ref-valued bindings (`EncodedSid` / blank-node `Sid`) +/// alongside `t`, mirroring how literal-valued objects already carry the +/// metadata. Callers outside history mode pass `None`. pub(crate) fn late_materialized_object_binding( o_type: u16, o_key: u64, p_id: u32, t: i64, o_i: u32, + op: Option, ) -> Option { let ot = OType::from_u16(o_type); match ot.decode_kind() { - DecodeKind::IriRef => Some(Binding::EncodedSid { s_id: o_key }), - DecodeKind::BlankNode => Some(Binding::Sid(Sid::new(0, format!("_:b{o_key}")))), + DecodeKind::IriRef => Some(Binding::EncodedSid { + s_id: o_key, + t: Some(t), + op, + }), + DecodeKind::BlankNode => Some(Binding::Sid { + sid: Sid::new(0, format!("_:b{o_key}")), + t: Some(t), + op, + }), DecodeKind::StringDict => { let (dt_id, lang_id) = if ot.is_lang_string() { (DatatypeDictId::LANG_STRING.as_u16(), ot.payload()) @@ -74,15 +89,22 @@ pub(crate) fn late_materialized_object_binding( } } +/// Build a materialized object binding for the binary scan path. +/// +/// `op` mirrors the meaning in `late_materialized_object_binding`: it is +/// `Some(...)` only in history mode and is threaded onto the ref- and +/// literal-valued binding alike, so downstream `T(?v)` / `OP(?v)` +/// resolves uniformly across object types. pub(crate) fn materialized_object_binding( store: &BinaryIndexStore, o_type: u16, p_id: u32, val: FlakeValue, t: Option, + op: Option, ) -> Binding { match val { - FlakeValue::Ref(sid) => Binding::Sid(sid), + FlakeValue::Ref(sid) => Binding::Sid { sid, t, op }, other => { let dtc = match store.resolve_lang_tag(o_type).map(Arc::from) { Some(lang) => DatatypeConstraint::LangTag(lang), @@ -96,7 +118,7 @@ pub(crate) fn materialized_object_binding( val: other, dtc, t, - op: None, + op, p_id: Some(p_id), } } diff --git a/fluree-db-query/src/optional.rs b/fluree-db-query/src/optional.rs index 7a6d0fb98..16297a14c 100644 --- a/fluree-db-query/src/optional.rs +++ b/fluree-db-query/src/optional.rs @@ -275,8 +275,8 @@ impl PatternOptionalBuilder { return Ok(None); }; match binding { - Binding::EncodedSid { s_id } => Ok(Some(*s_id)), - Binding::Sid(sid) => store + Binding::EncodedSid { s_id, .. } => Ok(Some(*s_id)), + Binding::Sid { sid, .. } => store .find_subject_id_by_parts(sid.namespace_code, &sid.name) .map_err(|e| QueryError::execution(format!("find_subject_id_by_parts: {e}"))), _ => Ok(None), @@ -303,14 +303,14 @@ impl PatternOptionalBuilder { match instr.position { PatternPosition::Subject => { match binding { - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { pattern.s = Ref::Sid(sid.clone()); } Binding::IriMatch { iri, .. } | Binding::Iri(iri) => { // Use Ref::Iri so scan can encode for each target ledger pattern.s = Ref::Iri(iri.clone()); } - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { // Late materialized subject ID: resolve to IRI for correlation. // Uses novelty-aware BinaryGraphView via ctx.graph_view(). let gv = ctx.graph_view().ok_or_else(|| { @@ -331,7 +331,7 @@ impl PatternOptionalBuilder { } PatternPosition::Predicate => { match binding { - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { pattern.p = Ref::Sid(sid.clone()); } Binding::IriMatch { iri, .. } | Binding::Iri(iri) => { @@ -345,7 +345,7 @@ impl PatternOptionalBuilder { } PatternPosition::Object => { match binding { - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { pattern.o = Term::Sid(sid.clone()); } Binding::IriMatch { iri, .. } | Binding::Iri(iri) => { @@ -524,13 +524,13 @@ impl OptionalBuilder for PatternOptionalBuilder { } let binding = required_batch.get_by_col(row, instr.left_col); return match binding { - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { let mut v = Vec::with_capacity(1 + 8); v.push(b'S'); v.extend_from_slice(&s_id.to_le_bytes()); Ok(Some(v.into_boxed_slice())) } - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { // Fallback stable key: namespace code + suffix bytes. let mut v = Vec::with_capacity(1 + 2 + sid.name_str().len()); v.push(b's'); @@ -636,8 +636,8 @@ impl GroupedPatternOptionalBuilder { return Ok(None); }; match binding { - Binding::EncodedSid { s_id } => Ok(Some(*s_id)), - Binding::Sid(sid) => store + Binding::EncodedSid { s_id, .. } => Ok(Some(*s_id)), + Binding::Sid { sid, .. } => store .find_subject_id_by_parts(sid.namespace_code, &sid.name) .map_err(|e| QueryError::execution(format!("find_subject_id_by_parts: {e}"))), _ => Ok(None), @@ -870,13 +870,13 @@ impl OptionalBuilder for GroupedPatternOptionalBuilder { let binding = required_batch.get_by_col(row, self.subject_left_col); let _ = ctx; match binding { - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { let mut v = Vec::with_capacity(1 + 8); v.push(b'S'); v.extend_from_slice(&s_id.to_le_bytes()); Ok(Some(v.into_boxed_slice())) } - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { let mut v = Vec::with_capacity(1 + 2 + sid.name_str().len()); v.push(b's'); v.extend_from_slice(&sid.namespace_code.to_le_bytes()); @@ -1750,7 +1750,7 @@ mod tests { // Create a batch with normal bindings let columns_normal = vec![ - vec![Binding::Sid(Sid::new(1, "alice"))], + vec![Binding::sid(Sid::new(1, "alice"))], vec![Binding::lit( FlakeValue::String("Alice".to_string()), Sid::new(2, "string"), @@ -1788,7 +1788,7 @@ mod tests { // Create a required batch with one row let columns = vec![ - vec![Binding::Sid(Sid::new(1, "alice"))], + vec![Binding::sid(Sid::new(1, "alice"))], vec![Binding::lit( FlakeValue::String("Alice".to_string()), Sid::new(2, "string"), diff --git a/fluree-db-query/src/parse/lower.rs b/fluree-db-query/src/parse/lower.rs index 7eaaabf30..86e4e9849 100644 --- a/fluree-db-query/src/parse/lower.rs +++ b/fluree-db-query/src/parse/lower.rs @@ -608,7 +608,7 @@ fn lower_values_cell(cell: &UnresolvedValue, encoder: &E) -> Resu let sid = encoder .encode_iri(iri) .ok_or_else(|| ParseError::UnknownNamespace(iri.to_string()))?; - Ok(Binding::Sid(sid)) + Ok(Binding::sid(sid)) } UnresolvedValue::Literal { value, dtc } => { // Build initial FlakeValue from the literal diff --git a/fluree-db-query/src/parse/node_map.rs b/fluree-db-query/src/parse/node_map.rs index 6ea023f5f..3cf8fc75c 100644 --- a/fluree-db-query/src/parse/node_map.rs +++ b/fluree-db-query/src/parse/node_map.rs @@ -138,12 +138,15 @@ fn add_metadata_bind_pattern( Ok(()) } -/// Add a FILTER pattern for a constant comparison (e.g., `op(?val) = "assert"`). +/// Add a FILTER pattern for a constant comparison (e.g., `op(?val) = true`). /// -/// Creates: `FILTER(func(?object_var) = constant_value)` +/// Creates: `FILTER(func(?object_var) = constant)`. The constant is +/// supplied as an already-built `UnresolvedExpression` so callers can +/// pick the right literal type (string for `@type`/`@language`, +/// boolean for `@op`). fn add_metadata_filter_pattern( func_name: &str, - constant_value: &str, + constant: crate::parse::ast::UnresolvedExpression, object: &UnresolvedTerm, query: &mut UnresolvedQuery, pattern: &UnresolvedTriplePattern, @@ -164,7 +167,7 @@ fn add_metadata_filter_pattern( }; let filter_expr = UnresolvedExpression::Call { func: Arc::from("="), - args: vec![func_expr, UnresolvedExpression::string(constant_value)], + args: vec![func_expr, constant], }; let filter_pattern = UnresolvedPattern::Filter(filter_expr); @@ -770,30 +773,33 @@ fn parse_property( )?; } - // Handle @op: variable creates BIND, constant creates FILTER - if let Some(op_var) = parsed.op_var { - if is_variable(&op_var) { - // Variable binding: BIND(op(?val) AS ?op) - add_metadata_bind_pattern( - "op", - op_var, - &object, - query, - &pattern, - &mut pattern_added, - "@op variable binding", - )?; - } else { - // Constant filter: FILTER(op(?val) = "assert") - add_metadata_filter_pattern( - "op", - &op_var, - &object, - query, - &pattern, - &mut pattern_added, - "@op filter", - )?; + // Handle @op: variable creates BIND, boolean constant creates FILTER. + if let Some(op_ann) = parsed.op_var { + match op_ann { + OpAnnotation::Variable(var) => { + // BIND(op(?val) AS ?op) + add_metadata_bind_pattern( + "op", + var, + &object, + query, + &pattern, + &mut pattern_added, + "@op variable binding", + )?; + } + OpAnnotation::Constant(b) => { + // FILTER(op(?val) = true|false) + add_metadata_filter_pattern( + "op", + crate::parse::ast::UnresolvedExpression::boolean(b), + &object, + query, + &pattern, + &mut pattern_added, + "@op filter", + )?; + } } } @@ -896,9 +902,22 @@ struct ParsedValueObject { dt_var: Option>, /// Transaction time variable (if @t is "?var") t_var: Option>, - /// Operation variable (if @op is "?var") - for history queries - /// Binds to "assert" or "retract" indicating the flake operation - op_var: Option>, + /// Operation annotation for history queries — either a variable + /// binding or a boolean constant filter (`true` = assert, + /// `false` = retract). + op_var: Option, +} + +/// `@op` annotation parsed from a value object. +/// +/// `Variable` produces a `BIND(op(?v) AS ?out)`; `Constant` produces a +/// `FILTER(op(?v) = )`. The on-disk `Flake.op` is a boolean, so +/// the user-facing surface mirrors that — assert is `true`, retract is +/// `false`. +#[derive(Debug, Clone)] +enum OpAnnotation { + Variable(Arc), + Constant(bool), } fn parse_value_object( @@ -984,45 +1003,33 @@ fn parse_value_object( None }; - // Optional @op - Fluree-specific operation binding for history queries (must be a variable like "?op") - // In history mode, this binds to "assert" or "retract" indicating the flake's operation type. - // Can also be a constant "assert" or "retract" to filter by operation type. - let explicit_op_var: Option> = if let Some(op_val) = obj.get("@op") { - let op_str = op_val.as_str().ok_or_else(|| { - ParseError::InvalidWhere( - "@op must be a string (variable like \"?op\" or constant \"assert\"/\"retract\")" - .to_string(), - ) - })?; - if is_variable(op_str) { - Some(Arc::from(op_str)) - } else if op_str == "assert" || op_str == "retract" { - // Constant filter value - store as-is for filter generation - Some(Arc::from(op_str)) - } else { - return Err(ParseError::InvalidWhere( - "@op must be a variable (e.g., \"?op\") or one of \"assert\", \"retract\"" - .to_string(), - )); + // Optional @op - Fluree-specific operation binding for history queries. + // Variable form (`"?op"`) creates a BIND that binds to a boolean + // (`true` = assert, `false` = retract); constant form (`true` or + // `false`) creates a FILTER selecting only matching events. + let explicit_op_var: Option = if let Some(op_val) = obj.get("@op") { + match op_val { + JsonValue::String(s) if is_variable(s) => { + Some(OpAnnotation::Variable(Arc::from(s.as_str()))) + } + JsonValue::Bool(b) => Some(OpAnnotation::Constant(*b)), + _ => { + return Err(ParseError::InvalidWhere( + "@op must be a variable (e.g., \"?op\") or a boolean constant (true = assert, false = retract)" + .to_string(), + )); + } } } else { None }; - // If @type is @id, treat @value as IRI/ref + // If @type is @id, treat @value as IRI/ref. Both `@t` and `@op` are + // permitted here: ref-valued object bindings carry the same history + // metadata as literals (see `Binding::Sid { t, op }`), so the + // parser-generated `BIND(t(?v) AS ?t)` / `BIND(op(?v) AS ?op)` + // resolve uniformly. if matches!(explicit_dt.as_deref(), Some("@id")) { - // @t is not supported with @type: "@id" (ref objects don't carry t in bindings) - if explicit_t_var.is_some() { - return Err(ParseError::InvalidWhere( - "@t binding is not supported with @type: \"@id\"; @t only applies to literal values".to_string() - )); - } - // @op is not supported with @type: "@id" (ref objects don't carry op in bindings) - if explicit_op_var.is_some() { - return Err(ParseError::InvalidWhere( - "@op binding is not supported with @type: \"@id\"; @op only applies to literal values".to_string() - )); - } let s = value_val.as_str().ok_or_else(|| { ParseError::InvalidWhere("@value must be a string when @type is @id".to_string()) })?; @@ -1031,8 +1038,8 @@ fn parse_value_object( dtc: None, lang_var: None, dt_var: None, - t_var: None, - op_var: None, + t_var: explicit_t_var, + op_var: explicit_op_var, }); } diff --git a/fluree-db-query/src/property_join.rs b/fluree-db-query/src/property_join.rs index cb80018dd..07f4b5c50 100644 --- a/fluree-db-query/src/property_join.rs +++ b/fluree-db-query/src/property_join.rs @@ -238,9 +238,7 @@ impl PropertyJoinOperator { pred_idx: usize, probe_match: BatchedSubjectProbeMatch, ) -> Result<()> { - let subject = Binding::EncodedSid { - s_id: probe_match.subject_id, - }; + let subject = Binding::encoded_sid(probe_match.subject_id); if let Some(key) = Self::subject_key(ctx, &subject)? { if let Some(entry) = all_subject_values.get_mut(&key) { entry.1 |= 1u64 << pred_idx; @@ -260,9 +258,7 @@ impl PropertyJoinOperator { all_subject_values: &mut FxHashMap>)>, spot_match: BatchedSpotStarMatch, ) -> Result<()> { - let subject = Binding::EncodedSid { - s_id: spot_match.subject_id, - }; + let subject = Binding::encoded_sid(spot_match.subject_id); if let Some(key) = Self::subject_key(ctx, &subject)? { if let Some(entry) = all_subject_values.get_mut(&key) { entry.1 |= 1u64 << spot_match.predicate_idx; @@ -461,8 +457,8 @@ impl PropertyJoinOperator { fn subject_key_single(subject: &Binding) -> Option { match subject { - Binding::EncodedSid { s_id } => Some(SubjectKey::Id(*s_id)), - Binding::Sid(sid) => Some(SubjectKey::Sid(sid.clone())), + Binding::EncodedSid { s_id, .. } => Some(SubjectKey::Id(*s_id)), + Binding::Sid { sid, .. } => Some(SubjectKey::Sid(sid.clone())), Binding::IriMatch { primary_sid, .. } => Some(SubjectKey::Sid(primary_sid.clone())), Binding::Iri(iri) => Some(SubjectKey::Iri(iri.clone())), _ => None, @@ -476,7 +472,7 @@ impl PropertyJoinOperator { Ok(match subject { Binding::IriMatch { iri, .. } => Some(SubjectKey::Iri(iri.clone())), Binding::Iri(iri) => Some(SubjectKey::Iri(iri.clone())), - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { // In dataset mode, use canonical IRI strings as join keys. // Prefer decoding within the active ledger when available. let Some(iri) = ctx @@ -488,7 +484,7 @@ impl PropertyJoinOperator { }; Some(SubjectKey::Iri(Arc::from(iri))) } - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { // Resolve to canonical IRI for cross-ledger comparison. // Novelty-aware via ctx.resolve_subject_iri(). match ctx.resolve_subject_iri(*s_id) { @@ -1108,7 +1104,7 @@ mod tests { #[test] fn test_subject_key_single_prefers_encoded_ids() { // Single-ledger mode should not require IRI decoding for EncodedSid. - let key = PropertyJoinOperator::subject_key_single(&Binding::EncodedSid { s_id: 42 }); + let key = PropertyJoinOperator::subject_key_single(&Binding::encoded_sid(42)); assert!(matches!(key, Some(SubjectKey::Id(42)))); } @@ -1118,10 +1114,10 @@ mod tests { let op = PropertyJoinOperator::new(&patterns, HashMap::new()).unwrap(); let subject_sid = Sid::new(1, "alice"); - let subject_binding = Binding::Sid(subject_sid.clone()); + let subject_binding = Binding::sid(subject_sid.clone()); let values = vec![ - vec![Binding::Sid(Sid::new(200, "Alice"))], // name - vec![Binding::Sid(Sid::new(201, "30"))], // age + vec![Binding::sid(Sid::new(200, "Alice"))], // name + vec![Binding::sid(Sid::new(201, "30"))], // age ]; let rows = PropertyJoinOperator::generate_rows( @@ -1132,7 +1128,7 @@ mod tests { ); assert_eq!(rows.len(), 1); assert_eq!(rows[0].len(), 3); - assert!(matches!(&rows[0][0], Binding::Sid(s) if *s == subject_sid)); + assert!(matches!(&rows[0][0], Binding::Sid { sid: s, .. } if *s == subject_sid)); } #[test] @@ -1140,16 +1136,16 @@ mod tests { let patterns = make_property_join_patterns(); let op = PropertyJoinOperator::new(&patterns, HashMap::new()).unwrap(); - let subject_binding = Binding::Sid(Sid::new(1, "alice")); + let subject_binding = Binding::sid(Sid::new(1, "alice")); let values = vec![ vec![ - Binding::Sid(Sid::new(200, "Alice")), - Binding::Sid(Sid::new(201, "Alicia")), + Binding::sid(Sid::new(200, "Alice")), + Binding::sid(Sid::new(201, "Alicia")), ], // 2 names vec![ - Binding::Sid(Sid::new(300, "30")), - Binding::Sid(Sid::new(301, "31")), - Binding::Sid(Sid::new(302, "32")), + Binding::sid(Sid::new(300, "30")), + Binding::sid(Sid::new(301, "31")), + Binding::sid(Sid::new(302, "32")), ], // 3 ages ]; @@ -1168,9 +1164,9 @@ mod tests { let patterns = make_property_join_patterns(); let op = PropertyJoinOperator::new(&patterns, HashMap::new()).unwrap(); - let subject_binding = Binding::Sid(Sid::new(1, "alice")); + let subject_binding = Binding::sid(Sid::new(1, "alice")); let values = vec![ - vec![Binding::Sid(Sid::new(200, "Alice"))], // has name + vec![Binding::sid(Sid::new(200, "Alice"))], // has name vec![], // no age ]; @@ -1186,9 +1182,9 @@ mod tests { #[test] fn test_generate_rows_missing_optional_uses_poisoned() { - let subject_binding = Binding::Sid(Sid::new(1, "alice")); + let subject_binding = Binding::sid(Sid::new(1, "alice")); let values = vec![ - vec![Binding::Sid(Sid::new(200, "Alice"))], // required name + vec![Binding::sid(Sid::new(200, "Alice"))], // required name vec![], // optional probability ]; diff --git a/fluree-db-query/src/property_path.rs b/fluree-db-query/src/property_path.rs index 81277e5a3..e53934ce2 100644 --- a/fluree-db-query/src/property_path.rs +++ b/fluree-db-query/src/property_path.rs @@ -466,7 +466,7 @@ impl PropertyPathOperator { Ref::Sid(s) => Some(s.clone()), Ref::Iri(iri) => db_for_encode.encode_iri(iri), Ref::Var(_) => binding.and_then(|b| match b { - Binding::Sid(s) => Some(s.clone()), + Binding::Sid { sid: s, .. } => Some(s.clone()), Binding::IriMatch { iri, .. } => db_for_encode.encode_iri(iri), Binding::Iri(iri) => db_for_encode.encode_iri(iri), _ => None, @@ -492,9 +492,9 @@ impl PropertyPathOperator { if let Some(col) = child_batch.column(*var) { row.push(col[row_idx].clone()); } else if Some(*var) == obj_var { - row.push(Binding::Sid(obj.clone())); + row.push(Binding::sid(obj.clone())); } else if Some(*var) == subj_var { - row.push(Binding::Sid(start.clone())); + row.push(Binding::sid(start.clone())); } else { row.push(Binding::Unbound); } @@ -514,9 +514,9 @@ impl PropertyPathOperator { if let Some(col) = child_batch.column(*var) { row.push(col[row_idx].clone()); } else if Some(*var) == subj_var { - row.push(Binding::Sid(subj.clone())); + row.push(Binding::sid(subj.clone())); } else if Some(*var) == obj_var { - row.push(Binding::Sid(target.clone())); + row.push(Binding::sid(target.clone())); } else { row.push(Binding::Unbound); } @@ -555,9 +555,9 @@ impl PropertyPathOperator { if let Some(col) = child_batch.column(*var) { row.push(col[row_idx].clone()); } else if Some(*var) == subj_var { - row.push(Binding::Sid(subj.clone())); + row.push(Binding::sid(subj.clone())); } else if Some(*var) == obj_var { - row.push(Binding::Sid(obj.clone())); + row.push(Binding::sid(obj.clone())); } else { row.push(Binding::Unbound); } @@ -635,9 +635,9 @@ impl Operator for PropertyPathOperator { for (subj, obj) in batch_results { for (col_idx, var) in self.in_schema.iter().enumerate() { if Some(*var) == subj_var { - columns[col_idx].push(Binding::Sid(subj.clone())); + columns[col_idx].push(Binding::sid(subj.clone())); } else if Some(*var) == obj_var { - columns[col_idx].push(Binding::Sid(obj.clone())); + columns[col_idx].push(Binding::sid(obj.clone())); } else { columns[col_idx].push(Binding::Unbound); } diff --git a/fluree-db-query/src/s2_search.rs b/fluree-db-query/src/s2_search.rs index 475aa0efa..2d08da359 100644 --- a/fluree-db-query/src/s2_search.rs +++ b/fluree-db-query/src/s2_search.rs @@ -424,9 +424,7 @@ impl Operator for S2SearchOperator { } // Add subject binding (encoded for late materialization) - row[subject_pos] = Binding::EncodedSid { - s_id: result.subject_id, - }; + row[subject_pos] = Binding::encoded_sid(result.subject_id); // Add distance binding if requested if let Some(dist_pos) = distance_pos { @@ -466,9 +464,7 @@ impl Operator for S2SearchOperator { } } - row[subject_pos] = Binding::EncodedSid { - s_id: result.subject_id, - }; + row[subject_pos] = Binding::encoded_sid(result.subject_id); for (col_idx, binding) in row.into_iter().enumerate() { columns[col_idx].push(binding); @@ -497,9 +493,7 @@ impl Operator for S2SearchOperator { } } - row[subject_pos] = Binding::EncodedSid { - s_id: result.subject_id, - }; + row[subject_pos] = Binding::encoded_sid(result.subject_id); for (col_idx, binding) in row.into_iter().enumerate() { columns[col_idx].push(binding); @@ -528,9 +522,7 @@ impl Operator for S2SearchOperator { } } - row[subject_pos] = Binding::EncodedSid { - s_id: result.subject_id, - }; + row[subject_pos] = Binding::encoded_sid(result.subject_id); for (col_idx, binding) in row.into_iter().enumerate() { columns[col_idx].push(binding); diff --git a/fluree-db-query/src/sort.rs b/fluree-db-query/src/sort.rs index 05515d572..0cc97c6ee 100644 --- a/fluree-db-query/src/sort.rs +++ b/fluree-db-query/src/sort.rs @@ -59,7 +59,7 @@ fn materialize_encoded_for_sort( .decode_value_from_kind(*o_kind, *o_key, *p_id, *dt_id, *lang_id) .ok()?; match val { - FlakeValue::Ref(sid) => Some(Binding::Sid(sid)), + FlakeValue::Ref(sid) => Some(Binding::sid(sid)), other => { let dt_sid = gv .store() @@ -82,16 +82,16 @@ fn materialize_encoded_for_sort( } } } - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { // BinaryGraphView::resolve_subject_sid handles novelty routing // and returns Sid directly (no IRI string + trie lookup). let sid = gv.resolve_subject_sid(*s_id).ok()?; - Some(Binding::Sid(sid)) + Some(Binding::sid(sid)) } Binding::EncodedPid { p_id } => { // Resolve to Sid for correct namespace/name ordering let iri = gv.store().resolve_predicate_iri(*p_id)?; - Some(Binding::Sid(gv.store().encode_iri(iri))) + Some(Binding::sid(gv.store().encode_iri(iri))) } _ => None, } @@ -211,7 +211,7 @@ pub fn compare_bindings(a: &Binding, b: &Binding) -> Ordering { // IRI types vs Lit types: IRI sorts before Lit ( - Binding::Sid(_) + Binding::Sid { .. } | Binding::IriMatch { .. } | Binding::Iri(_) | Binding::EncodedSid { .. } @@ -220,7 +220,7 @@ pub fn compare_bindings(a: &Binding, b: &Binding) -> Ordering { ) => Ordering::Less, ( Binding::Lit { .. } | Binding::EncodedLit { .. }, - Binding::Sid(_) + Binding::Sid { .. } | Binding::IriMatch { .. } | Binding::Iri(_) | Binding::EncodedSid { .. } @@ -228,29 +228,35 @@ pub fn compare_bindings(a: &Binding, b: &Binding) -> Ordering { ) => Ordering::Greater, // Within IRI types: compare by concrete value or ID - (Binding::Sid(a), Binding::Sid(b)) => compare_sids(a, b), + (Binding::Sid { sid: a, .. }, Binding::Sid { sid: b, .. }) => compare_sids(a, b), (Binding::IriMatch { iri: a, .. }, Binding::IriMatch { iri: b, .. }) => a.cmp(b), (Binding::Iri(a), Binding::Iri(b)) => a.cmp(b), - (Binding::EncodedSid { s_id: a }, Binding::EncodedSid { s_id: b }) => a.cmp(b), + (Binding::EncodedSid { s_id: a, .. }, Binding::EncodedSid { s_id: b, .. }) => a.cmp(b), (Binding::EncodedPid { p_id: a }, Binding::EncodedPid { p_id: b }) => a.cmp(b), // Cross-IRI type comparisons: Sid < IriMatch/Iri < EncodedSid/EncodedPid // (Prefer materialized over encoded for consistent ordering) - (Binding::Sid(_), Binding::IriMatch { .. } | Binding::Iri(_)) => Ordering::Less, - (Binding::IriMatch { .. } | Binding::Iri(_), Binding::Sid(_)) => Ordering::Greater, + (Binding::Sid { sid: _, .. }, Binding::IriMatch { .. } | Binding::Iri(_)) => Ordering::Less, + (Binding::IriMatch { .. } | Binding::Iri(_), Binding::Sid { sid: _, .. }) => { + Ordering::Greater + } (Binding::IriMatch { iri: a, .. }, Binding::Iri(b)) => a.as_ref().cmp(b.as_ref()), (Binding::Iri(a), Binding::IriMatch { iri: b, .. }) => a.as_ref().cmp(b.as_ref()), // Encoded IRI types sort after decoded types when mixed ( - Binding::Sid(_) | Binding::IriMatch { .. } | Binding::Iri(_), + Binding::Sid { .. } | Binding::IriMatch { .. } | Binding::Iri(_), Binding::EncodedSid { .. } | Binding::EncodedPid { .. }, ) => Ordering::Less, ( Binding::EncodedSid { .. } | Binding::EncodedPid { .. }, - Binding::Sid(_) | Binding::IriMatch { .. } | Binding::Iri(_), + Binding::Sid { .. } | Binding::IriMatch { .. } | Binding::Iri(_), ) => Ordering::Greater, // EncodedSid vs EncodedPid: compare by ID (they're in same class) - (Binding::EncodedSid { s_id }, Binding::EncodedPid { p_id }) => s_id.cmp(&(*p_id as u64)), - (Binding::EncodedPid { p_id }, Binding::EncodedSid { s_id }) => (*p_id as u64).cmp(s_id), + (Binding::EncodedSid { s_id, .. }, Binding::EncodedPid { p_id }) => { + s_id.cmp(&(*p_id as u64)) + } + (Binding::EncodedPid { p_id }, Binding::EncodedSid { s_id, .. }) => { + (*p_id as u64).cmp(s_id) + } // Within Lit types: compare by value (Binding::Lit { val: v1, .. }, Binding::Lit { val: v2, .. }) => { @@ -911,9 +917,9 @@ mod tests { #[test] fn test_compare_bindings_sid() { - let sid1 = Binding::Sid(Sid::new(1, "apple")); - let sid2 = Binding::Sid(Sid::new(1, "banana")); - let sid3 = Binding::Sid(Sid::new(2, "apple")); + let sid1 = Binding::sid(Sid::new(1, "apple")); + let sid2 = Binding::sid(Sid::new(1, "banana")); + let sid3 = Binding::sid(Sid::new(2, "apple")); assert_eq!(compare_bindings(&sid1, &sid1), Ordering::Equal); assert_eq!(compare_bindings(&sid1, &sid2), Ordering::Less); // apple < banana diff --git a/fluree-db-query/src/stats_query.rs b/fluree-db-query/src/stats_query.rs index 2ec23969c..02a49772f 100644 --- a/fluree-db-query/src/stats_query.rs +++ b/fluree-db-query/src/stats_query.rs @@ -59,7 +59,7 @@ impl StatsCountByPredicateOperator { let Some(pred_sid) = pred_sid else { continue; }; - let pred = Binding::Sid(pred_sid); + let pred = Binding::sid(pred_sid); let count = Binding::lit(FlakeValue::Long(data.count as i64), dt.clone()); out.push((pred, count)); } @@ -70,7 +70,7 @@ impl StatsCountByPredicateOperator { if !self.stats.properties.is_empty() { let mut out = Vec::with_capacity(self.stats.properties.len()); for (sid, data) in &self.stats.properties { - let pred = Binding::Sid(sid.clone()); + let pred = Binding::sid(sid.clone()); let count = Binding::lit(FlakeValue::Long(data.count as i64), dt.clone()); out.push((pred, count)); } diff --git a/fluree-db-query/src/values.rs b/fluree-db-query/src/values.rs index 7df1166b3..17f45207f 100644 --- a/fluree-db-query/src/values.rs +++ b/fluree-db-query/src/values.rs @@ -247,8 +247,8 @@ fn bindings_compatible_for_values(ctx: &ExecutionContext<'_>, a: &Binding, b: &B match (a, b) { // Compare SID to IRI-bearing bindings by decoding SID via primary db. - (Binding::Sid(sid), Binding::Iri(iri) | Binding::IriMatch { iri, .. }) - | (Binding::Iri(iri) | Binding::IriMatch { iri, .. }, Binding::Sid(sid)) => ctx + (Binding::Sid { sid, .. }, Binding::Iri(iri) | Binding::IriMatch { iri, .. }) + | (Binding::Iri(iri) | Binding::IriMatch { iri, .. }, Binding::Sid { sid, .. }) => ctx .active_snapshot .decode_sid(sid) .map(|decoded| decoded == iri.as_ref()) diff --git a/fluree-db-query/src/vector/operator.rs b/fluree-db-query/src/vector/operator.rs index 0b5e74462..f5e9d0dc1 100644 --- a/fluree-db-query/src/vector/operator.rs +++ b/fluree-db-query/src/vector/operator.rs @@ -178,7 +178,7 @@ impl VectorSearchOperator { }, Some(Binding::EncodedLit { .. }) => Ok(None), Some( - Binding::Sid(_) + Binding::Sid { .. } | Binding::IriMatch { .. } | Binding::Iri(_) | Binding::Grouped(_) diff --git a/fluree-db-query/tests/correctness_tests.rs b/fluree-db-query/tests/correctness_tests.rs index 10d217854..fed50578d 100644 --- a/fluree-db-query/tests/correctness_tests.rs +++ b/fluree-db-query/tests/correctness_tests.rs @@ -102,7 +102,7 @@ async fn test_optional_poison_blocks_subsequent() { let required_schema: Arc<[VarId]> = Arc::from(vec![s].into_boxed_slice()); let required_batch = Batch::new( required_schema.clone(), - vec![vec![Binding::Sid(Sid::new(100, "alice"))]], + vec![vec![Binding::sid(Sid::new(100, "alice"))]], ) .unwrap(); diff --git a/fluree-db-query/tests/groupby_aggregate_tests.rs b/fluree-db-query/tests/groupby_aggregate_tests.rs index f807fbcc7..1d40a2ed0 100644 --- a/fluree-db-query/tests/groupby_aggregate_tests.rs +++ b/fluree-db-query/tests/groupby_aggregate_tests.rs @@ -73,23 +73,23 @@ async fn test_group_by_with_count() { rows: vec![ vec![ Binding::lit(FlakeValue::String("NYC".into()), xsd_string()), - Binding::Sid(Sid::new(100, "alice")), + Binding::sid(Sid::new(100, "alice")), ], vec![ Binding::lit(FlakeValue::String("NYC".into()), xsd_string()), - Binding::Sid(Sid::new(100, "bob")), + Binding::sid(Sid::new(100, "bob")), ], vec![ Binding::lit(FlakeValue::String("LA".into()), xsd_string()), - Binding::Sid(Sid::new(100, "carol")), + Binding::sid(Sid::new(100, "carol")), ], vec![ Binding::lit(FlakeValue::String("LA".into()), xsd_string()), - Binding::Sid(Sid::new(100, "dan")), + Binding::sid(Sid::new(100, "dan")), ], vec![ Binding::lit(FlakeValue::String("LA".into()), xsd_string()), - Binding::Sid(Sid::new(100, "eve")), + Binding::sid(Sid::new(100, "eve")), ], ], }], @@ -226,23 +226,23 @@ async fn test_group_by_with_having() { rows: vec![ vec![ Binding::lit(FlakeValue::String("NYC".into()), xsd_string()), - Binding::Sid(Sid::new(100, "alice")), + Binding::sid(Sid::new(100, "alice")), ], vec![ Binding::lit(FlakeValue::String("NYC".into()), xsd_string()), - Binding::Sid(Sid::new(100, "bob")), + Binding::sid(Sid::new(100, "bob")), ], vec![ Binding::lit(FlakeValue::String("LA".into()), xsd_string()), - Binding::Sid(Sid::new(100, "carol")), + Binding::sid(Sid::new(100, "carol")), ], vec![ Binding::lit(FlakeValue::String("LA".into()), xsd_string()), - Binding::Sid(Sid::new(100, "dan")), + Binding::sid(Sid::new(100, "dan")), ], vec![ Binding::lit(FlakeValue::String("LA".into()), xsd_string()), - Binding::Sid(Sid::new(100, "eve")), + Binding::sid(Sid::new(100, "eve")), ], ], }], @@ -547,11 +547,11 @@ async fn test_order_by_on_grouped_var_errors() { rows: vec![ vec![ Binding::lit(FlakeValue::String("NYC".into()), xsd_string()), - Binding::Sid(Sid::new(100, "alice")), + Binding::sid(Sid::new(100, "alice")), ], vec![ Binding::lit(FlakeValue::String("NYC".into()), xsd_string()), - Binding::Sid(Sid::new(100, "bob")), + Binding::sid(Sid::new(100, "bob")), ], ], }], @@ -587,11 +587,11 @@ async fn test_aggregate_on_group_by_key_errors() { rows: vec![ vec![ Binding::lit(FlakeValue::String("NYC".into()), xsd_string()), - Binding::Sid(Sid::new(100, "alice")), + Binding::sid(Sid::new(100, "alice")), ], vec![ Binding::lit(FlakeValue::String("NYC".into()), xsd_string()), - Binding::Sid(Sid::new(100, "bob")), + Binding::sid(Sid::new(100, "bob")), ], ], }], diff --git a/fluree-db-query/tests/owl2rl_property_rules_integration_tests.rs b/fluree-db-query/tests/owl2rl_property_rules_integration_tests.rs index 49433e386..fdd527875 100644 --- a/fluree-db-query/tests/owl2rl_property_rules_integration_tests.rs +++ b/fluree-db-query/tests/owl2rl_property_rules_integration_tests.rs @@ -341,7 +341,7 @@ async fn owl2rl_domain_range_and_chain_visible_via_execute_with_overlay() { for row_idx in 0..batch.len() { let row = batch.row_view(row_idx).unwrap(); match row.get(x) { - Some(Binding::Sid(sid)) => got.push(sid.clone()), + Some(Binding::Sid { sid, .. }) => got.push(sid.clone()), Some(other) => panic!("Expected Sid binding for ?x, got {other:?}"), None => panic!("Expected binding for ?x"), } @@ -379,7 +379,7 @@ async fn owl2rl_domain_range_and_chain_visible_via_execute_with_overlay() { for row_idx in 0..batch.len() { let row = batch.row_view(row_idx).unwrap(); match row.get(o) { - Some(Binding::Sid(sid)) => got_o.push(sid.clone()), + Some(Binding::Sid { sid, .. }) => got_o.push(sid.clone()), Some(other) => panic!("Expected Sid binding for ?o, got {other:?}"), None => panic!("Expected binding for ?o"), } diff --git a/fluree-db-query/tests/values_bind_union_tests.rs b/fluree-db-query/tests/values_bind_union_tests.rs index 9c9e0e270..f06074f8f 100644 --- a/fluree-db-query/tests/values_bind_union_tests.rs +++ b/fluree-db-query/tests/values_bind_union_tests.rs @@ -73,7 +73,7 @@ async fn test_values_first_then_join() { vec![ Pattern::Values { vars: vec![VarId(0)], // ?s - rows: vec![vec![Binding::Sid(sid1)], vec![Binding::Sid(sid2)]], + rows: vec![vec![Binding::sid(sid1)], vec![Binding::sid(sid2)]], }, Pattern::Triple(make_triple_pattern(VarId(0), "name", VarId(1))), ], diff --git a/fluree-db-sparql/src/lower/describe.rs b/fluree-db-sparql/src/lower/describe.rs index 422eb3ee9..015ae88b6 100644 --- a/fluree-db-sparql/src/lower/describe.rs +++ b/fluree-db-sparql/src/lower/describe.rs @@ -67,7 +67,7 @@ impl LoweringContext<'_, E> { if !explicit_sids.is_empty() { let rows = explicit_sids .into_iter() - .map(|sid| vec![Binding::Sid(sid)]) + .map(|sid| vec![Binding::sid(sid)]) .collect(); branches.push(vec![Pattern::Values { vars: vec![describe_var], diff --git a/fluree-db-sparql/src/lower/term.rs b/fluree-db-sparql/src/lower/term.rs index 8f79befca..d0c42cf03 100644 --- a/fluree-db-sparql/src/lower/term.rs +++ b/fluree-db-sparql/src/lower/term.rs @@ -305,7 +305,7 @@ impl LoweringContext<'_, E> { .encoder .encode_iri(&full_iri) .ok_or_else(|| LowerError::unknown_namespace(&full_iri, iri.span))?; - Ok(Binding::Sid(sid)) + Ok(Binding::sid(sid)) } SparqlTerm::Literal(lit) => match &lit.value { LiteralValue::Simple(s) => Ok(Binding::lit( diff --git a/fluree-db-transact/src/generate/flakes.rs b/fluree-db-transact/src/generate/flakes.rs index 4d3e3356e..9e9591cf0 100644 --- a/fluree-db-transact/src/generate/flakes.rs +++ b/fluree-db-transact/src/generate/flakes.rs @@ -230,7 +230,7 @@ impl<'a> FlakeGenerator<'a> { } if let Some(binding) = bindings.get(row, *var_id) { match binding { - Binding::Sid(sid) => Ok(Some(sid.clone())), + Binding::Sid { sid, .. } => Ok(Some(sid.clone())), Binding::IriMatch { primary_sid, .. } => Ok(Some(primary_sid.clone())), Binding::Unbound | Binding::Poisoned => Ok(None), Binding::Grouped(_) => Err(TransactError::InvalidTerm( @@ -281,7 +281,7 @@ impl<'a> FlakeGenerator<'a> { } if let Some(binding) = bindings.get(row, *var_id) { match binding { - Binding::Sid(sid) => Ok(Some(sid.clone())), + Binding::Sid { sid, .. } => Ok(Some(sid.clone())), Binding::IriMatch { primary_sid, .. } => Ok(Some(primary_sid.clone())), Binding::Unbound | Binding::Poisoned => Ok(None), Binding::Grouped(_) => Err(TransactError::InvalidTerm( @@ -339,7 +339,7 @@ impl<'a> FlakeGenerator<'a> { } if let Some(binding) = bindings.get(row, *var_id) { match binding { - Binding::Sid(sid) => { + Binding::Sid { sid, .. } => { Ok((Some(FlakeValue::Ref(sid.clone())), Some(DT_ID.clone()))) } Binding::IriMatch { primary_sid, .. } => { diff --git a/fluree-db-transact/src/stage.rs b/fluree-db-transact/src/stage.rs index c2e4c07ef..394dd6854 100644 --- a/fluree-db-transact/src/stage.rs +++ b/fluree-db-transact/src/stage.rs @@ -1091,7 +1091,7 @@ fn materialize_one_binding( ) -> Result<()> { let store_ref = gv.store(); match b { - Binding::EncodedSid { s_id } => { + Binding::EncodedSid { s_id, .. } => { let iri = store_ref.resolve_subject_iri(*s_id).map_err(|e| { TransactError::Query(fluree_db_query::QueryError::Internal(format!( "resolve_subject_iri: {e}" @@ -1102,7 +1102,7 @@ fn materialize_one_binding( "encode_iri returned None for subject IRI: {iri}" ))) })?; - *b = Binding::Sid(sid); + *b = Binding::sid(sid); } Binding::EncodedPid { p_id } => { let iri = store_ref.resolve_predicate_iri(*p_id).ok_or_else(|| { @@ -1115,7 +1115,7 @@ fn materialize_one_binding( "encode_iri returned None for predicate IRI: {iri}" ))) })?; - *b = Binding::Sid(sid); + *b = Binding::sid(sid); } Binding::EncodedLit { o_kind, @@ -1137,7 +1137,7 @@ fn materialize_one_binding( })?; match val { FlakeValue::Ref(sid) => { - *b = Binding::Sid(sid); + *b = Binding::sid(sid); } other => { let dt_sid = store_ref @@ -1220,7 +1220,7 @@ fn binding_to_flake_object( materializer: Option<&mut fluree_db_query::Materializer>, ) -> Option<(FlakeValue, Sid)> { match binding { - Binding::Sid(sid) => Some((FlakeValue::Ref(sid.clone()), Sid::new(1, "id"))), + Binding::Sid { sid, .. } => Some((FlakeValue::Ref(sid.clone()), Sid::new(1, "id"))), Binding::IriMatch { primary_sid, .. } => { Some((FlakeValue::Ref(primary_sid.clone()), Sid::new(1, "id"))) } @@ -1256,7 +1256,7 @@ fn binding_to_flake_object( /// Convert a TemplateTerm to a Binding for VALUES clause fn template_term_to_binding(term: &TemplateTerm) -> Result { match term { - TemplateTerm::Sid(sid) => Ok(Binding::Sid(sid.clone())), + TemplateTerm::Sid(sid) => Ok(Binding::sid(sid.clone())), TemplateTerm::Value(val) => { let dt = infer_datatype(val); Ok(Binding::lit(val.clone(), dt)) @@ -1852,16 +1852,14 @@ mod tests { fn column_needs_materialization_detects_each_encoded_variant() { // Already-concrete bindings — must NOT trigger rewrite. let concrete = vec![ - Binding::Sid(Sid::new(1, "a")), + Binding::sid(Sid::new(1, "a")), Binding::Unbound, Binding::Poisoned, ]; assert!(!column_needs_materialization(&concrete)); // Each Encoded* variant must trigger rewrite individually. - assert!(column_needs_materialization(&[Binding::EncodedSid { - s_id: 7 - }])); + assert!(column_needs_materialization(&[Binding::encoded_sid(7)])); assert!(column_needs_materialization(&[Binding::EncodedPid { p_id: 3 }])); @@ -1877,8 +1875,8 @@ mod tests { // A column with a single encoded entry among many concrete entries // must still trigger — early-exit on first hit. - let mut mixed = vec![Binding::Sid(Sid::new(1, "a")); 8]; - mixed.push(Binding::EncodedSid { s_id: 1 }); + let mut mixed = vec![Binding::sid(Sid::new(1, "a")); 8]; + mixed.push(Binding::encoded_sid(1)); mixed.extend(std::iter::repeat_n(Binding::Unbound, 4)); assert!(column_needs_materialization(&mixed)); }