Skip to main content

qualia_cli/
sparql.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Deserialize)]
4#[allow(dead_code)]
5pub struct RpcRequest {
6    pub jsonrpc: String,
7    pub method: String,
8    pub params: RpcParams,
9    pub id: u64,
10}
11
12#[derive(Deserialize)]
13#[allow(dead_code)]
14pub struct RpcParams {
15    pub query: Option<String>,
16    pub token: Option<String>,
17}
18
19#[derive(Serialize)]
20#[allow(dead_code)]
21pub struct RpcResponse {
22    pub jsonrpc: String,
23    pub result: Option<serde_json::Value>,
24    pub error: Option<String>,
25    pub id: u64,
26}
27
28/// Execute a SPARQL (or SPARQL-Star) query string against a `.q42` vault.
29///
30/// Range operators are tried first. Vaults larger than
31/// [`RESIDENT_QUERY_MAX_BYTES`] never call `read_q42_quins` / `read_all_quins`;
32/// they fail if no range plan applies. Smaller files may still use the
33/// resident executor.
34pub fn run_sparql_query(vault: &std::path::Path, query_str: &str) {
35    use qualia_core_db::q42_volume::{Q42QueryMode, RESIDENT_QUERY_MAX_BYTES};
36    use qualia_core_db::sparql_executor::QueryExecutor;
37    use qualia_core_db::sparql_planner::QueryPlanner;
38
39    let (query, ctx) = match qualia_core_db::sparql_parser::parse_sparql(query_str) {
40        Ok(pair) => pair,
41        Err(e) => {
42            eprintln!("SPARQL parse error: {e}");
43            return;
44        }
45    };
46
47    let plan = match QueryPlanner::plan(&query, &ctx) {
48        Ok(p) => p,
49        Err(e) => {
50            eprintln!("SPARQL plan error: {e}");
51            return;
52        }
53    };
54
55    let file_bytes = std::fs::metadata(vault).map(|m| m.len()).unwrap_or(0);
56    let mode = Q42QueryMode::for_file_bytes(file_bytes);
57
58    match try_range_query(vault, &plan, &ctx) {
59        Ok(()) => return,
60        Err(RangeDispatch::NoPlan(reason)) => {
61            if !mode.allows_read_all_quins() {
62                eprintln!(
63                    "Range SPARQL required: vault is {file_bytes} bytes (limit {RESIDENT_QUERY_MAX_BYTES}); no range plan applies ({reason}). Resident fallback to read_q42_quins is forbidden."
64                );
65                return;
66            }
67            eprintln!("Range SPARQL unavailable; using resident compatibility executor: {reason}");
68        }
69        Err(RangeDispatch::Failed(error)) => {
70            if !mode.allows_read_all_quins() {
71                eprintln!(
72                    "Range SPARQL failed; resident fallback forbidden ({file_bytes} bytes > {RESIDENT_QUERY_MAX_BYTES}): {error}"
73                );
74                return;
75            }
76            eprintln!("Range SPARQL unavailable; using resident compatibility executor: {error}");
77        }
78    }
79
80    if !mode.allows_read_all_quins() {
81        eprintln!(
82            "Range SPARQL required: vault is {file_bytes} bytes (limit {RESIDENT_QUERY_MAX_BYTES}); refusing read_q42_quins / read_all_quins."
83        );
84        return;
85    }
86
87    let quins = match qualia_core_db::q42_reader::read_q42_quins(vault) {
88        Ok(quins) => quins,
89        Err(e) => {
90            eprintln!("Warning: could not open vault '{}': {e}", vault.display());
91            Vec::new()
92        }
93    };
94    let lex = qualia_core_db::q42_lex::Q42Lexicon::load_for_q42(vault).ok();
95
96    if quins.is_empty() {
97        eprintln!(
98            "Warning: vault '{}' is empty or could not be opened.",
99            vault.display()
100        );
101    }
102
103    let executor = QueryExecutor::new(&quins);
104    match executor.execute(&plan, &ctx) {
105        Err(e) => eprintln!("SPARQL execution error: {e}"),
106        Ok(rows) => {
107            println!("{} result(s) from {}", rows.len(), vault.display());
108            for (i, row) in rows.iter().enumerate() {
109                print!("[{i:>4}]");
110                for (slot_idx, slot) in row.slots.iter().enumerate() {
111                    if let Some(v) = slot {
112                        match lex.as_ref().and_then(|l| l.lookup(*v)) {
113                            Some(s) => print!("  ?v{slot_idx}={s}"),
114                            None => print!("  ?v{slot_idx}=0x{v:016x}"),
115                        }
116                    }
117                }
118                println!();
119            }
120        }
121    }
122}
123
124enum RangeDispatch {
125    NoPlan(String),
126    Failed(String),
127}
128
129fn try_range_query(
130    vault: &std::path::Path,
131    plan: &qualia_core_db::sparql_planner::ExecutionPlan,
132    ctx: &qualia_core_db::sparql_ast::SparqlQueryContext,
133) -> Result<(), RangeDispatch> {
134    if let Ok(range_plan) = qualia_core_db::Q42RangeOptionalPlan::from_execution_plan(plan) {
135        return run_range_optional_query(vault, range_plan, ctx).map_err(RangeDispatch::Failed);
136    }
137    if let Ok(range_plan) = qualia_core_db::Q42RangeUnionPlan::from_execution_plan(plan) {
138        return run_range_union_query(vault, range_plan, ctx).map_err(RangeDispatch::Failed);
139    }
140    if let Ok(range_plan) = qualia_core_db::Q42RangeBindPlan::from_execution_plan(plan) {
141        return run_range_bind_query(vault, range_plan, ctx).map_err(RangeDispatch::Failed);
142    }
143    if let Ok(range_plan) = qualia_core_db::Q42RangeHashJoinPlan::from_execution_plan(plan) {
144        match run_range_hash_join_query(vault, range_plan, ctx) {
145            Ok(()) => return Ok(()),
146            Err(error) if error.contains("planner must keep NestedLoopJoin") => {
147                return run_range_join_select_query(vault, range_plan.as_join_select(), ctx)
148                    .map_err(RangeDispatch::Failed);
149            }
150            Err(error) => return Err(RangeDispatch::Failed(error)),
151        }
152    }
153    if let Ok(range_plan) = qualia_core_db::Q42RangeJoinSelectPlan::from_execution_plan(plan) {
154        return run_range_join_select_query(vault, range_plan, ctx).map_err(RangeDispatch::Failed);
155    }
156    if let Ok(range_plan) =
157        qualia_core_db::sparql_executor::Q42RangeTripleSelectPlan::from_execution_plan(plan)
158    {
159        return run_range_triple_query(vault, range_plan, ctx).map_err(RangeDispatch::Failed);
160    }
161    if let Ok(range_plan) =
162        qualia_core_db::sparql_executor::Q42RangeNestedLoopJoinPlan::from_execution_plan(plan)
163    {
164        return run_range_join_query(vault, range_plan, ctx).map_err(RangeDispatch::Failed);
165    }
166    Err(RangeDispatch::NoPlan(
167        "query is not a range-executable TripleScan, join, OPTIONAL, UNION, or BIND".into(),
168    ))
169}
170
171fn run_range_triple_query(
172    vault: &std::path::Path,
173    plan: qualia_core_db::sparql_executor::Q42RangeTripleSelectPlan,
174    ctx: &qualia_core_db::sparql_ast::SparqlQueryContext,
175) -> Result<(), String> {
176    use qualia_core_db::q42_volume::{
177        LocalFileRangeSource, Q42RangeVolume, Q42RangeVolumeSet, Q42VolumeSegment,
178    };
179    use qualia_core_db::sparql_ast::BindingRow;
180    use qualia_core_db::sparql_executor::{
181        execute_range_triple_select_page_into, execute_range_volume_set_triple_select_page_into,
182        Q42RangeTripleSelectCursor, Q42RangeVolumeSetTripleSelectCursor,
183    };
184
185    const PAGE_ROWS: usize = 128;
186    let source = LocalFileRangeSource::open(vault).map_err(|error| error.to_string())?;
187    let root = Q42RangeVolume::open(source).map_err(|error| error.to_string())?;
188    let lexicon = qualia_core_db::q42_lex::Q42Lexicon::load_for_q42(vault).ok();
189    let mut compressed = [0u8; qualia_core_db::q42_volume::MAX_COMPRESSED_SUPERBLOCK_SIZE];
190    let mut decoded = [0u8; qualia_core_db::q42_volume::SUPERBLOCK_SIZE];
191    let mut quins = [qualia_core_db::NQuin::default(); PAGE_ROWS];
192    let mut rows = [BindingRow::default(); PAGE_ROWS];
193    let mut total = 0usize;
194
195    if root
196        .volume_manifest_length()
197        .map_err(|error| error.to_string())?
198        .is_some()
199    {
200        let parent = vault.parent().unwrap_or_else(|| std::path::Path::new("."));
201        let factory =
202            |entry: &Q42VolumeSegment| LocalFileRangeSource::open(&parent.join(&entry.locator));
203        let mut set =
204            Q42RangeVolumeSet::open_root(&root, &factory).map_err(|error| error.to_string())?;
205        set.attach_lexicon_segments(&|entry| {
206            LocalFileRangeSource::open(&parent.join(&entry.locator))
207        })
208        .map_err(|error| error.to_string())?;
209        let mut cursor = Q42RangeVolumeSetTripleSelectCursor::default();
210        loop {
211            let page = execute_range_volume_set_triple_select_page_into(
212                &set,
213                plan,
214                ctx,
215                cursor,
216                &mut compressed,
217                &mut decoded,
218                &mut quins,
219                &mut rows,
220            )?;
221            print_range_rows(&rows[..page.returned], lexicon.as_ref());
222            total += page.returned;
223            let Some(next) = page.next_cursor else {
224                break;
225            };
226            cursor = next;
227        }
228    } else {
229        let mut cursor = Q42RangeTripleSelectCursor::default();
230        loop {
231            let page = execute_range_triple_select_page_into(
232                &root,
233                plan,
234                ctx,
235                cursor,
236                &mut compressed,
237                &mut decoded,
238                &mut quins,
239                &mut rows,
240            )?;
241            print_range_rows(&rows[..page.returned], lexicon.as_ref());
242            total += page.returned;
243            let Some(next) = page.next_cursor else {
244                break;
245            };
246            cursor = next;
247        }
248    }
249    println!("{total} result(s) from {} (range-backed)", vault.display());
250    Ok(())
251}
252
253/// Execute a two-pattern nested-loop join directly from physical SuperBlocks.
254/// The supported plan deliberately excludes project/filter/limit wrappers: the
255/// resident executor remains authoritative for those until their range-native
256/// equivalents can preserve every SPARQL semantic.
257fn run_range_join_query(
258    vault: &std::path::Path,
259    plan: qualia_core_db::sparql_executor::Q42RangeNestedLoopJoinPlan,
260    ctx: &qualia_core_db::sparql_ast::SparqlQueryContext,
261) -> Result<(), String> {
262    use qualia_core_db::q42_volume::{
263        LocalFileRangeSource, Q42RangeVolume, Q42RangeVolumeSet, Q42VolumeSegment,
264    };
265    use qualia_core_db::sparql_ast::BindingRow;
266    use qualia_core_db::sparql_executor::{
267        execute_range_nested_loop_join_page_into,
268        execute_range_volume_set_nested_loop_join_page_into, Q42RangeNestedLoopJoinState,
269        Q42RangeVolumeSetNestedLoopJoinState,
270    };
271
272    const PAGE_ROWS: usize = 128;
273    let source = LocalFileRangeSource::open(vault).map_err(|error| error.to_string())?;
274    let root = Q42RangeVolume::open(source).map_err(|error| error.to_string())?;
275    let lexicon = qualia_core_db::q42_lex::Q42Lexicon::load_for_q42(vault).ok();
276    let mut compressed = [0u8; qualia_core_db::q42_volume::MAX_COMPRESSED_SUPERBLOCK_SIZE];
277    let mut decoded = [0u8; qualia_core_db::q42_volume::SUPERBLOCK_SIZE];
278    let mut quins = [qualia_core_db::NQuin::default(); PAGE_ROWS];
279    let mut left_rows = [BindingRow::default(); PAGE_ROWS];
280    let mut right_rows = [BindingRow::default(); PAGE_ROWS];
281    let mut rows = [BindingRow::default(); PAGE_ROWS];
282    let mut total = 0usize;
283
284    if root
285        .volume_manifest_length()
286        .map_err(|error| error.to_string())?
287        .is_some()
288    {
289        let parent = vault.parent().unwrap_or_else(|| std::path::Path::new("."));
290        let factory =
291            |entry: &Q42VolumeSegment| LocalFileRangeSource::open(&parent.join(&entry.locator));
292        let volumes =
293            Q42RangeVolumeSet::open_root(&root, &factory).map_err(|error| error.to_string())?;
294        let mut state = Q42RangeVolumeSetNestedLoopJoinState::default();
295        loop {
296            let page = execute_range_volume_set_nested_loop_join_page_into(
297                &volumes,
298                plan,
299                ctx,
300                &mut state,
301                &mut compressed,
302                &mut decoded,
303                &mut quins,
304                &mut left_rows,
305                &mut right_rows,
306                &mut rows,
307            )?;
308            print_range_rows(&rows[..page.returned], lexicon.as_ref());
309            total += page.returned;
310            if page.done {
311                break;
312            }
313        }
314    } else {
315        let mut state = Q42RangeNestedLoopJoinState::default();
316        loop {
317            let page = execute_range_nested_loop_join_page_into(
318                &root,
319                plan,
320                ctx,
321                &mut state,
322                &mut compressed,
323                &mut decoded,
324                &mut quins,
325                &mut left_rows,
326                &mut right_rows,
327                &mut rows,
328            )?;
329            print_range_rows(&rows[..page.returned], lexicon.as_ref());
330            total += page.returned;
331            if page.done {
332                break;
333            }
334        }
335    }
336    println!(
337        "{total} result(s) from {} (range-backed join)",
338        vault.display()
339    );
340    Ok(())
341}
342
343fn run_range_join_select_query(
344    vault: &std::path::Path,
345    plan: qualia_core_db::Q42RangeJoinSelectPlan,
346    ctx: &qualia_core_db::sparql_ast::SparqlQueryContext,
347) -> Result<(), String> {
348    use qualia_core_db::q42_volume::{
349        LocalFileRangeSource, Q42RangeVolume, Q42RangeVolumeSet, Q42VolumeSegment,
350    };
351    use qualia_core_db::sparql_ast::BindingRow;
352    use qualia_core_db::{
353        execute_range_join_select_page_into, execute_range_volume_set_join_select_page_into,
354        Q42RangeJoinSelectState, Q42RangeVolumeSetJoinSelectState,
355    };
356
357    const PAGE_ROWS: usize = 128;
358    let source = LocalFileRangeSource::open(vault).map_err(|error| error.to_string())?;
359    let root = Q42RangeVolume::open(source).map_err(|error| error.to_string())?;
360    let lexicon = qualia_core_db::q42_lex::Q42Lexicon::load_for_q42(vault).ok();
361    let mut compressed = [0u8; qualia_core_db::q42_volume::MAX_COMPRESSED_SUPERBLOCK_SIZE];
362    let mut decoded = [0u8; qualia_core_db::q42_volume::SUPERBLOCK_SIZE];
363    let mut quins = [qualia_core_db::NQuin::default(); PAGE_ROWS];
364    let mut left_rows = [BindingRow::default(); PAGE_ROWS];
365    let mut right_rows = [BindingRow::default(); PAGE_ROWS];
366    let mut join_out = [BindingRow::default(); PAGE_ROWS];
367    let mut rows = [BindingRow::default(); PAGE_ROWS];
368    let mut total = 0usize;
369
370    if root
371        .volume_manifest_length()
372        .map_err(|error| error.to_string())?
373        .is_some()
374    {
375        let parent = vault.parent().unwrap_or_else(|| std::path::Path::new("."));
376        let factory =
377            |entry: &Q42VolumeSegment| LocalFileRangeSource::open(&parent.join(&entry.locator));
378        let volumes =
379            Q42RangeVolumeSet::open_root(&root, &factory).map_err(|error| error.to_string())?;
380        let mut state = Q42RangeVolumeSetJoinSelectState::default();
381        loop {
382            let page = execute_range_volume_set_join_select_page_into(
383                &volumes,
384                plan,
385                ctx,
386                &mut state,
387                &mut compressed,
388                &mut decoded,
389                &mut quins,
390                &mut left_rows,
391                &mut right_rows,
392                &mut join_out,
393                &mut rows,
394            )?;
395            print_range_rows(&rows[..page.returned], lexicon.as_ref());
396            total += page.returned;
397            if page.done {
398                break;
399            }
400        }
401    } else {
402        let mut state = Q42RangeJoinSelectState::default();
403        loop {
404            let page = execute_range_join_select_page_into(
405                &root,
406                plan,
407                ctx,
408                &mut state,
409                &mut compressed,
410                &mut decoded,
411                &mut quins,
412                &mut left_rows,
413                &mut right_rows,
414                &mut join_out,
415                &mut rows,
416            )?;
417            print_range_rows(&rows[..page.returned], lexicon.as_ref());
418            total += page.returned;
419            if page.done {
420                break;
421            }
422        }
423    }
424    println!(
425        "{total} result(s) from {} (range-backed join-select)",
426        vault.display()
427    );
428    Ok(())
429}
430
431fn run_range_optional_query(
432    vault: &std::path::Path,
433    plan: qualia_core_db::Q42RangeOptionalPlan,
434    ctx: &qualia_core_db::sparql_ast::SparqlQueryContext,
435) -> Result<(), String> {
436    use qualia_core_db::q42_volume::{
437        LocalFileRangeSource, Q42RangeVolume, Q42RangeVolumeSet, Q42VolumeSegment,
438    };
439    use qualia_core_db::sparql_ast::BindingRow;
440    use qualia_core_db::{
441        execute_range_optional_page_into, execute_range_volume_set_optional_page_into,
442        Q42RangeOptionalState, Q42RangeVolumeSetOptionalState,
443    };
444
445    const PAGE_ROWS: usize = 128;
446    let source = LocalFileRangeSource::open(vault).map_err(|error| error.to_string())?;
447    let root = Q42RangeVolume::open(source).map_err(|error| error.to_string())?;
448    let lexicon = qualia_core_db::q42_lex::Q42Lexicon::load_for_q42(vault).ok();
449    let mut compressed = [0u8; qualia_core_db::q42_volume::MAX_COMPRESSED_SUPERBLOCK_SIZE];
450    let mut decoded = [0u8; qualia_core_db::q42_volume::SUPERBLOCK_SIZE];
451    let mut quins = [qualia_core_db::NQuin::default(); PAGE_ROWS];
452    let mut left_rows = [BindingRow::default(); PAGE_ROWS];
453    let mut right_rows = [BindingRow::default(); PAGE_ROWS];
454    let mut join_out = [BindingRow::default(); PAGE_ROWS];
455    let mut rows = [BindingRow::default(); PAGE_ROWS];
456    let mut total = 0usize;
457
458    if root
459        .volume_manifest_length()
460        .map_err(|error| error.to_string())?
461        .is_some()
462    {
463        let parent = vault.parent().unwrap_or_else(|| std::path::Path::new("."));
464        let factory =
465            |entry: &Q42VolumeSegment| LocalFileRangeSource::open(&parent.join(&entry.locator));
466        let volumes =
467            Q42RangeVolumeSet::open_root(&root, &factory).map_err(|error| error.to_string())?;
468        let mut state = Q42RangeVolumeSetOptionalState::default();
469        loop {
470            let page = execute_range_volume_set_optional_page_into(
471                &volumes,
472                plan,
473                ctx,
474                &mut state,
475                &mut compressed,
476                &mut decoded,
477                &mut quins,
478                &mut left_rows,
479                &mut right_rows,
480                &mut join_out,
481                &mut rows,
482            )?;
483            print_range_rows(&rows[..page.returned], lexicon.as_ref());
484            total += page.returned;
485            if page.done {
486                break;
487            }
488        }
489    } else {
490        let mut state = Q42RangeOptionalState::default();
491        loop {
492            let page = execute_range_optional_page_into(
493                &root,
494                plan,
495                ctx,
496                &mut state,
497                &mut compressed,
498                &mut decoded,
499                &mut quins,
500                &mut left_rows,
501                &mut right_rows,
502                &mut join_out,
503                &mut rows,
504            )?;
505            print_range_rows(&rows[..page.returned], lexicon.as_ref());
506            total += page.returned;
507            if page.done {
508                break;
509            }
510        }
511    }
512    println!(
513        "{total} result(s) from {} (range-backed optional)",
514        vault.display()
515    );
516    Ok(())
517}
518
519fn run_range_union_query(
520    vault: &std::path::Path,
521    plan: qualia_core_db::Q42RangeUnionPlan,
522    ctx: &qualia_core_db::sparql_ast::SparqlQueryContext,
523) -> Result<(), String> {
524    use qualia_core_db::q42_volume::{
525        LocalFileRangeSource, Q42RangeVolume, Q42RangeVolumeSet, Q42VolumeSegment,
526    };
527    use qualia_core_db::sparql_ast::BindingRow;
528    use qualia_core_db::{
529        execute_range_union_page_into, execute_range_volume_set_union_page_into, Q42RangeUnionState,
530        Q42RangeVolumeSetUnionState,
531    };
532
533    const PAGE_ROWS: usize = 128;
534    let source = LocalFileRangeSource::open(vault).map_err(|error| error.to_string())?;
535    let root = Q42RangeVolume::open(source).map_err(|error| error.to_string())?;
536    let lexicon = qualia_core_db::q42_lex::Q42Lexicon::load_for_q42(vault).ok();
537    let mut compressed = [0u8; qualia_core_db::q42_volume::MAX_COMPRESSED_SUPERBLOCK_SIZE];
538    let mut decoded = [0u8; qualia_core_db::q42_volume::SUPERBLOCK_SIZE];
539    let mut quins = [qualia_core_db::NQuin::default(); PAGE_ROWS];
540    let mut raw = [BindingRow::default(); PAGE_ROWS];
541    let mut rows = [BindingRow::default(); PAGE_ROWS];
542    let mut total = 0usize;
543
544    if root
545        .volume_manifest_length()
546        .map_err(|error| error.to_string())?
547        .is_some()
548    {
549        let parent = vault.parent().unwrap_or_else(|| std::path::Path::new("."));
550        let factory =
551            |entry: &Q42VolumeSegment| LocalFileRangeSource::open(&parent.join(&entry.locator));
552        let volumes =
553            Q42RangeVolumeSet::open_root(&root, &factory).map_err(|error| error.to_string())?;
554        let mut state = Q42RangeVolumeSetUnionState::default();
555        loop {
556            let page = execute_range_volume_set_union_page_into(
557                &volumes,
558                plan,
559                ctx,
560                &mut state,
561                &mut compressed,
562                &mut decoded,
563                &mut quins,
564                &mut raw,
565                &mut rows,
566            )?;
567            print_range_rows(&rows[..page.returned], lexicon.as_ref());
568            total += page.returned;
569            if page.done {
570                break;
571            }
572        }
573    } else {
574        let mut state = Q42RangeUnionState::default();
575        loop {
576            let page = execute_range_union_page_into(
577                &root,
578                plan,
579                ctx,
580                &mut state,
581                &mut compressed,
582                &mut decoded,
583                &mut quins,
584                &mut raw,
585                &mut rows,
586            )?;
587            print_range_rows(&rows[..page.returned], lexicon.as_ref());
588            total += page.returned;
589            if page.done {
590                break;
591            }
592        }
593    }
594    println!(
595        "{total} result(s) from {} (range-backed union)",
596        vault.display()
597    );
598    Ok(())
599}
600
601fn run_range_bind_query(
602    vault: &std::path::Path,
603    plan: qualia_core_db::Q42RangeBindPlan,
604    ctx: &qualia_core_db::sparql_ast::SparqlQueryContext,
605) -> Result<(), String> {
606    use qualia_core_db::q42_volume::{
607        LocalFileRangeSource, Q42RangeVolume, Q42RangeVolumeSet, Q42VolumeSegment,
608    };
609    use qualia_core_db::sparql_ast::BindingRow;
610    use qualia_core_db::{
611        execute_range_bind_page_into, execute_range_volume_set_bind_page_into, Q42RangeBindState,
612        Q42RangeVolumeSetBindState,
613    };
614
615    const PAGE_ROWS: usize = 128;
616    let source = LocalFileRangeSource::open(vault).map_err(|error| error.to_string())?;
617    let root = Q42RangeVolume::open(source).map_err(|error| error.to_string())?;
618    let lexicon = qualia_core_db::q42_lex::Q42Lexicon::load_for_q42(vault).ok();
619    let mut compressed = [0u8; qualia_core_db::q42_volume::MAX_COMPRESSED_SUPERBLOCK_SIZE];
620    let mut decoded = [0u8; qualia_core_db::q42_volume::SUPERBLOCK_SIZE];
621    let mut quins = [qualia_core_db::NQuin::default(); PAGE_ROWS];
622    let mut raw = [BindingRow::default(); PAGE_ROWS];
623    let mut rows = [BindingRow::default(); PAGE_ROWS];
624    let mut total = 0usize;
625
626    if root
627        .volume_manifest_length()
628        .map_err(|error| error.to_string())?
629        .is_some()
630    {
631        let parent = vault.parent().unwrap_or_else(|| std::path::Path::new("."));
632        let factory =
633            |entry: &Q42VolumeSegment| LocalFileRangeSource::open(&parent.join(&entry.locator));
634        let volumes =
635            Q42RangeVolumeSet::open_root(&root, &factory).map_err(|error| error.to_string())?;
636        let mut state = Q42RangeVolumeSetBindState::default();
637        loop {
638            let page = execute_range_volume_set_bind_page_into(
639                &volumes,
640                plan,
641                ctx,
642                &mut state,
643                &mut compressed,
644                &mut decoded,
645                &mut quins,
646                &mut raw,
647                &mut rows,
648            )?;
649            print_range_rows(&rows[..page.returned], lexicon.as_ref());
650            total += page.returned;
651            if page.done {
652                break;
653            }
654        }
655    } else {
656        let mut state = Q42RangeBindState::default();
657        loop {
658            let page = execute_range_bind_page_into(
659                &root,
660                plan,
661                ctx,
662                &mut state,
663                &mut compressed,
664                &mut decoded,
665                &mut quins,
666                &mut raw,
667                &mut rows,
668            )?;
669            print_range_rows(&rows[..page.returned], lexicon.as_ref());
670            total += page.returned;
671            if page.done {
672                break;
673            }
674        }
675    }
676    println!(
677        "{total} result(s) from {} (range-backed bind)",
678        vault.display()
679    );
680    Ok(())
681}
682
683fn run_range_hash_join_query(
684    vault: &std::path::Path,
685    plan: qualia_core_db::Q42RangeHashJoinPlan,
686    ctx: &qualia_core_db::sparql_ast::SparqlQueryContext,
687) -> Result<(), String> {
688    use qualia_core_db::q42_volume::{
689        LocalFileRangeSource, Q42RangeVolume, Q42RangeVolumeSet, Q42VolumeSegment,
690    };
691    use qualia_core_db::sparql_ast::BindingRow;
692    use qualia_core_db::{
693        execute_range_hash_join_page_into, execute_range_volume_set_hash_join_page_into,
694        Q42RangeHashJoinSlot, Q42RangeHashJoinState, Q42RangeVolumeSetHashJoinState,
695    };
696
697    const PAGE_ROWS: usize = 128;
698    const HASH_CAP: usize = 256;
699    let source = LocalFileRangeSource::open(vault).map_err(|error| error.to_string())?;
700    let root = Q42RangeVolume::open(source).map_err(|error| error.to_string())?;
701    let lexicon = qualia_core_db::q42_lex::Q42Lexicon::load_for_q42(vault).ok();
702    let mut compressed = [0u8; qualia_core_db::q42_volume::MAX_COMPRESSED_SUPERBLOCK_SIZE];
703    let mut decoded = [0u8; qualia_core_db::q42_volume::SUPERBLOCK_SIZE];
704    let mut quins = [qualia_core_db::NQuin::default(); PAGE_ROWS];
705    let mut probe_rows = [BindingRow::default(); PAGE_ROWS];
706    let mut join_out = [BindingRow::default(); PAGE_ROWS];
707    let mut rows = [BindingRow::default(); PAGE_ROWS];
708    let mut table = [Q42RangeHashJoinSlot::default(); HASH_CAP];
709    let mut total = 0usize;
710
711    if root
712        .volume_manifest_length()
713        .map_err(|error| error.to_string())?
714        .is_some()
715    {
716        let parent = vault.parent().unwrap_or_else(|| std::path::Path::new("."));
717        let factory =
718            |entry: &Q42VolumeSegment| LocalFileRangeSource::open(&parent.join(&entry.locator));
719        let volumes =
720            Q42RangeVolumeSet::open_root(&root, &factory).map_err(|error| error.to_string())?;
721        let mut state = Q42RangeVolumeSetHashJoinState::default();
722        loop {
723            let page = execute_range_volume_set_hash_join_page_into(
724                &volumes,
725                plan,
726                ctx,
727                &mut state,
728                &mut table,
729                &mut compressed,
730                &mut decoded,
731                &mut quins,
732                &mut probe_rows,
733                &mut join_out,
734                &mut rows,
735            )?;
736            print_range_rows(&rows[..page.returned], lexicon.as_ref());
737            total += page.returned;
738            if page.done {
739                break;
740            }
741        }
742    } else {
743        let mut state = Q42RangeHashJoinState::default();
744        loop {
745            let page = execute_range_hash_join_page_into(
746                &root,
747                plan,
748                ctx,
749                &mut state,
750                &mut table,
751                &mut compressed,
752                &mut decoded,
753                &mut quins,
754                &mut probe_rows,
755                &mut join_out,
756                &mut rows,
757            )?;
758            print_range_rows(&rows[..page.returned], lexicon.as_ref());
759            total += page.returned;
760            if page.done {
761                break;
762            }
763        }
764    }
765    println!(
766        "{total} result(s) from {} (range-backed hash-join)",
767        vault.display()
768    );
769    Ok(())
770}
771
772fn print_range_rows(
773    rows: &[qualia_core_db::sparql_ast::BindingRow],
774    lexicon: Option<&qualia_core_db::q42_lex::Q42Lexicon>,
775) {
776    for row in rows {
777        print!("[range]");
778        for (slot, value) in row.slots.iter().enumerate() {
779            if let Some(value) = value {
780                match lexicon.and_then(|lexicon| lexicon.lookup(*value)) {
781                    Some(text) => print!("  ?v{slot}={text}"),
782                    None => print!("  ?v{slot}=0x{value:016x}"),
783                }
784            }
785        }
786        println!();
787    }
788}