Skip to content

Commit 9e956a4

Browse files
Merge pull request #129 from triblespace/codex/implement-delta-macro-as-procedural-macro
Implement procedural delta macro
2 parents 43967fd + 9c48597 commit 9e956a4

5 files changed

Lines changed: 287 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- `entity!` now implemented as a procedural macro alongside `pattern!`.
1919
- `entity!` subsumes the old `entity_inner!` helper; macro invocations can
2020
optionally provide an existing `TribleSet`.
21+
- Implemented a procedural `delta!` macro for incremental query support.
2122
- Expanded documentation for the `pattern` procedural macro to ease maintenance, including detailed comments inside the implementation.
2223
- `EntityId` variants renamed to `Var` and `Lit` for consistency with field patterns.
2324
- `Workspace::checkout` now accepts commit ranges for convenient history queries.

INVENTORY.md

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,13 @@
44
- None at the moment.
55

66
## Completed Work
7-
- None yet. This file now tracks development ideas.
7+
- Implemented a `delta!` macro for incremental queries. The macro
8+
computes the difference between two `TribleSet`s and unions per-triple
9+
results so callers only see newly inserted data.
810

911
## Desired Functionality
1012
- Finalize the compressed zero-copy archive format currently mentioned as WIP.
1113
- Provide additional examples showcasing advanced queries and repository usage.
12-
- Add incremental query support building on the union constraint so
13-
results can update when datasets change without full recomputation.
14-
Namespaces will expose a `delta!` operator similar to `pattern!`
15-
that receives the previous and current `TribleSet`, calls `union!`
16-
internally and matches only the newly added tribles. See the book's
17-
[Incremental Queries](book/src/incremental-queries.md) chapter for
18-
the planned approach.
1914
- Explore replacing `CommitSelector` ranges with a set-based API
2015
built on commit reachability. The goal is to mirror git's revision
2116
selection semantics (similar to `rev-list` or `rev-parse`).

book/src/incremental-queries.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ delta while the remaining constraints see the full updated dataset. Each
1111
case yields the new solutions introduced by those additions and we then
1212
union all of the per‑constraint results.
1313

14-
To help express these delta queries at the macro level, namespaces will
14+
To help express these delta queries at the macro level, namespaces now
1515
offer a `delta!` operator. It behaves like `pattern!` but takes the
1616
previous and current `TribleSet`. The macro computes their difference
1717
and then calls `union!` internally to apply the resulting delta

src/namespace.rs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,15 @@ macro_rules! NS {
7575
};
7676
}
7777

78-
// TODO: incremental queries will eventually use a dedicated `delta!`
79-
// macro that applies semi-naive rewriting on a per-triple basis.
78+
#[macro_pub::macro_pub]
79+
macro_rules! delta {
80+
($prev:expr, $curr:expr, $pattern: tt) => {
81+
{
82+
::tribles_macros::delta!{ ::tribles, $mod_name, $prev, $curr, $pattern }
83+
}
84+
};
85+
}
86+
8087
}
8188
};
8289
}
@@ -199,4 +206,56 @@ mod tests {
199206
r
200207
);
201208
}
209+
210+
#[test]
211+
fn ns_delta() {
212+
let mut base = TribleSet::new();
213+
(0..10).for_each(|_| {
214+
let a = ufoid();
215+
let b = ufoid();
216+
base += literature::entity!(&a, {
217+
firstname: Name(EN).fake::<String>(),
218+
lastname: Name(EN).fake::<String>()
219+
});
220+
base += literature::entity!(&b, {
221+
title: Name(EN).fake::<String>(),
222+
author: &a
223+
});
224+
});
225+
226+
let mut updated = base.clone();
227+
let shakespeare = ufoid();
228+
let hamlet = ufoid();
229+
updated += literature::entity!(&shakespeare, {
230+
firstname: "William",
231+
lastname: "Shakespeare"
232+
});
233+
updated += literature::entity!(&hamlet, {
234+
title: "Hamlet",
235+
author: &shakespeare,
236+
quote: "To be, or not to be, that is the question.".to_blob().get_handle()
237+
});
238+
239+
let r: Vec<_> = find!(
240+
(author, hamlet, title),
241+
literature::delta!(&base, &updated, [
242+
{author @
243+
firstname: ("William"),
244+
lastname: ("Shakespeare")},
245+
{hamlet @
246+
title: title,
247+
author: author
248+
}])
249+
)
250+
.collect();
251+
252+
assert_eq!(
253+
vec![(
254+
shakespeare.to_value(),
255+
hamlet.to_value(),
256+
"Hamlet".to_value(),
257+
)],
258+
r
259+
);
260+
}
202261
}

tribles-macros/src/lib.rs

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,48 @@ pub fn entity(input: TokenStream) -> TokenStream {
344344
}
345345
}
346346

347+
/// Parsed input for the [`delta`] macro.
348+
///
349+
/// The invocation takes the form `crate_path, namespace_path, prev, curr, [..]`.
350+
/// `prev` and `curr` are expressions evaluating to [`TribleSet`]s. The pattern
351+
/// syntax matches that of [`pattern!`].
352+
struct DeltaInput {
353+
crate_path: Path,
354+
ns: Path,
355+
prev: Expr,
356+
curr: Expr,
357+
pattern: Vec<Entity>,
358+
}
359+
360+
impl Parse for DeltaInput {
361+
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
362+
let crate_path: Path = input.parse()?;
363+
input.parse::<Token![,]>()?;
364+
let ns: Path = input.parse()?;
365+
input.parse::<Token![,]>()?;
366+
let prev: Expr = input.parse()?;
367+
input.parse::<Token![,]>()?;
368+
let curr: Expr = input.parse()?;
369+
input.parse::<Token![,]>()?;
370+
let content;
371+
bracketed!(content in input);
372+
let mut pattern = Vec::new();
373+
while !content.is_empty() {
374+
pattern.push(content.parse()?);
375+
if content.peek(Token![,]) {
376+
content.parse::<Token![,]>()?;
377+
}
378+
}
379+
Ok(DeltaInput {
380+
crate_path,
381+
ns,
382+
prev,
383+
curr,
384+
pattern,
385+
})
386+
}
387+
}
388+
347389
fn entity_impl(input: TokenStream) -> syn::Result<TokenStream> {
348390
let EntityInput {
349391
crate_path,
@@ -406,3 +448,182 @@ fn entity_impl(input: TokenStream) -> syn::Result<TokenStream> {
406448

407449
Ok(output.into())
408450
}
451+
452+
/// Procedural implementation of the `delta!` macro.
453+
#[proc_macro]
454+
pub fn delta(input: TokenStream) -> TokenStream {
455+
match delta_impl(input) {
456+
Ok(ts) => ts,
457+
Err(e) => e.to_compile_error().into(),
458+
}
459+
}
460+
461+
fn delta_impl(input: TokenStream) -> syn::Result<TokenStream> {
462+
use std::collections::HashMap;
463+
464+
let DeltaInput {
465+
crate_path,
466+
ns,
467+
prev,
468+
curr,
469+
pattern,
470+
} = syn::parse(input)?;
471+
472+
// Identifiers used throughout the expansion
473+
let ctx_ident = format_ident!("__ctx", span = Span::call_site());
474+
let prev_ident = format_ident!("__prev", span = Span::call_site());
475+
let curr_ident = format_ident!("__curr", span = Span::call_site());
476+
let delta_ident = format_ident!("__delta", span = Span::call_site());
477+
478+
// Prepare declarations shared by all union branches
479+
let mut attr_decl_tokens = TokenStream2::new();
480+
let mut attr_const_tokens = TokenStream2::new();
481+
482+
let mut entity_decl_tokens = TokenStream2::new();
483+
let mut entity_const_tokens = TokenStream2::new();
484+
485+
let mut value_decl_tokens = TokenStream2::new();
486+
let mut value_const_tokens = TokenStream2::new();
487+
488+
struct TripleInfo {
489+
e_ident: Ident,
490+
a_ident: Ident,
491+
v_ident: Ident,
492+
}
493+
let mut triples: Vec<TripleInfo> = Vec::new();
494+
495+
let mut attr_map: HashMap<String, Ident> = HashMap::new();
496+
let mut attr_idx = 0usize;
497+
let mut entity_idx = 0usize;
498+
let mut value_idx = 0usize;
499+
500+
for entity in pattern {
501+
let e_ident = format_ident!("__e{}", entity_idx, span = Span::call_site());
502+
entity_idx += 1;
503+
match entity.id {
504+
Some(EntityId::Var(id)) => {
505+
entity_decl_tokens.extend(quote! { let #e_ident = #id; });
506+
}
507+
Some(EntityId::Lit(expr)) => {
508+
entity_decl_tokens.extend(quote! {
509+
let #e_ident: #crate_path::query::Variable<#crate_path::value::schemas::genid::GenId> = #ctx_ident.next_variable();
510+
});
511+
entity_const_tokens.extend(quote! {
512+
constraints.push({ let e: #crate_path::id::Id = #expr; Box::new(#e_ident.is(#crate_path::value::ToValue::to_value(e)))});
513+
});
514+
}
515+
None => {
516+
entity_decl_tokens.extend(quote! {
517+
let #e_ident: #crate_path::query::Variable<#crate_path::value::schemas::genid::GenId> = #ctx_ident.next_variable();
518+
});
519+
}
520+
}
521+
522+
for Field { name, value } in entity.fields {
523+
let field_ident = name;
524+
let a_ident = attr_map
525+
.entry(field_ident.to_string())
526+
.or_insert_with(|| {
527+
let ident = format_ident!("__a{}", attr_idx, span = Span::call_site());
528+
attr_idx += 1;
529+
attr_decl_tokens.extend(quote! {
530+
let #ident: #crate_path::query::Variable<#crate_path::value::schemas::genid::GenId> = #ctx_ident.next_variable();
531+
});
532+
attr_const_tokens.extend(quote! {
533+
constraints.push(Box::new(#ident.is(#crate_path::value::ToValue::to_value(ns::ids::#field_ident))));
534+
});
535+
ident
536+
})
537+
.clone();
538+
539+
let v_ident = format_ident!("__v{}", value_idx, span = Span::call_site());
540+
value_idx += 1;
541+
542+
match value {
543+
FieldValue::Lit(expr) => {
544+
let val_ident = format_ident!("__c{}", value_idx, span = Span::call_site());
545+
value_idx += 1;
546+
value_decl_tokens.extend(quote! {
547+
let #v_ident: #crate_path::query::Variable<ns::schemas::#field_ident> = #ctx_ident.next_variable();
548+
let #val_ident: #crate_path::value::Value<ns::schemas::#field_ident> = #crate_path::value::ToValue::to_value(#expr);
549+
});
550+
value_const_tokens.extend(quote! {
551+
constraints.push(Box::new(#v_ident.is(#val_ident)));
552+
});
553+
}
554+
FieldValue::Var(expr) => {
555+
value_decl_tokens.extend(quote! {
556+
let #v_ident: #crate_path::query::Variable<ns::schemas::#field_ident> = #expr;
557+
});
558+
}
559+
}
560+
561+
triples.push(TripleInfo {
562+
e_ident: e_ident.clone(),
563+
a_ident: a_ident.clone(),
564+
v_ident,
565+
});
566+
}
567+
}
568+
569+
let mut case_exprs: Vec<TokenStream2> = Vec::new();
570+
for delta_idx in 0..triples.len() {
571+
let mut triple_tokens = TokenStream2::new();
572+
for (
573+
idx,
574+
TripleInfo {
575+
e_ident,
576+
a_ident,
577+
v_ident,
578+
},
579+
) in triples.iter().enumerate()
580+
{
581+
let dataset = if idx == delta_idx {
582+
&delta_ident
583+
} else {
584+
&curr_ident
585+
};
586+
triple_tokens.extend(quote! {
587+
constraints.push(Box::new(#dataset.pattern(#e_ident, #a_ident, #v_ident)));
588+
});
589+
}
590+
591+
let case = quote! {
592+
{
593+
let mut constraints: Vec<Box<dyn #crate_path::query::Constraint>> = vec![];
594+
use #crate_path::query::TriblePattern;
595+
#triple_tokens
596+
#crate_path::query::intersectionconstraint::IntersectionConstraint::new(constraints)
597+
}
598+
};
599+
case_exprs.push(case);
600+
}
601+
602+
let union_expr = quote! {
603+
#crate_path::query::unionconstraint::UnionConstraint::new(vec![
604+
#(Box::new(#case_exprs) as Box<dyn #crate_path::query::Constraint>),*
605+
])
606+
};
607+
608+
let output = quote! {
609+
{
610+
let #ctx_ident = __local_find_context!();
611+
let #prev_ident = #prev;
612+
let #curr_ident = #curr;
613+
let #delta_ident = #curr_ident.difference(&#prev_ident);
614+
use #ns as ns;
615+
#attr_decl_tokens
616+
#entity_decl_tokens
617+
#value_decl_tokens
618+
let mut constraints: Vec<Box<dyn #crate_path::query::Constraint>> = vec![];
619+
use #crate_path::query::TriblePattern;
620+
#attr_const_tokens
621+
#entity_const_tokens
622+
#value_const_tokens
623+
constraints.push(Box::new(#union_expr));
624+
#crate_path::query::intersectionconstraint::IntersectionConstraint::new(constraints)
625+
}
626+
};
627+
628+
Ok(output.into())
629+
}

0 commit comments

Comments
 (0)