From 891ebfb0d8485087185fba3fbdcfabde22937895 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 00:43:23 +0000 Subject: [PATCH 01/14] Speed bench queries with AVET OrJoin, lazy bitsets, and AEVT scans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OrJoin constant branches: AVET entity-id union + AEVT value probe (no per-branch relation eval) - Defer (max_e+1) constant bitsets; AVET-fast NOT exclusion marking - AEVT array scan for [?e :attr ?v] patterns (q-rule follows) without Seq→list - Always unwrap Ref values via result_of_pattern_position on AEVT emit paths - Fix OrJoin relation merge: project each branch to join vars before union Co-authored-by: Tienson Qin --- impl/datascript.ml | 2 + impl/query_where.ml | 383 +++++++++++++++++++++++++++----------------- 2 files changed, 237 insertions(+), 148 deletions(-) diff --git a/impl/datascript.ml b/impl/datascript.ml index 97f3570..fa93964 100644 --- a/impl/datascript.ml +++ b/impl/datascript.ml @@ -1503,6 +1503,8 @@ module Query_where_impl = Query_where.Make (struct | None -> None | Some datom -> Some (Query.result_of_ref (Query.result_of_datom_v datom)) let aevt_attr_array = Db.aevt_attr_array + let aevt_duplicate_datoms db attr = + Option.value (Hashtbl.find_opt db.duplicate_aevt_by_attr attr) ~default:[] let find_entity_in_aevt_array = Db.find_entity_in_aevt_array end) diff --git a/impl/query_where.ml b/impl/query_where.ml index 2a39016..e4b8f91 100644 --- a/impl/query_where.ml +++ b/impl/query_where.ml @@ -34,6 +34,7 @@ module Make (Context : sig ('acc -> datom -> 'acc) -> 'acc -> db -> attr -> ?start:value -> ?stop:value -> unit -> 'acc val find_entity_attr_value : db -> entity_id -> attr -> query_result option val aevt_attr_array : db -> attr -> datom array option + val aevt_duplicate_datoms : db -> attr -> datom list val find_entity_in_aevt_array : datom array -> entity_id -> datom option end) = struct open Context @@ -354,23 +355,69 @@ end) = struct |> List.of_seq | _ -> invalid_arg "database source patterns expect 3, 4, or 5 terms" + let relation_of_aevt_var_var_pattern source_db e_var attr v_var = + (* Datahike-like OpScan on AEVT: walk attr arrays once, emit rows without Seq→list. *) + if query_evaluator_context.is_reverse_ref attr then + None + else + match aevt_attr_array source_db attr with + | None -> None + | Some primary -> + let attrs = unique_vars [ QVar e_var; QAttr attr; QVar v_var ] in + let lookup_vars = relation_lookup_vars source_db [ QVar e_var; QAttr attr; QVar v_var ] in + let emit_datom rows datom = + let value = result_of_pattern_position datom 2 in + match attrs with + | [ left; right ] when left = e_var && right = v_var -> + [ Result_entity datom.e; value ] :: rows + | [ left; right ] when left = v_var && right = e_var -> + [ value; Result_entity datom.e ] :: rows + | _ -> + (match binding_row attrs [ e_var, Result_entity datom.e; v_var, value ] with + | Some row -> row :: rows + | None -> rows) + in + let rows = ref [] in + for i = Array.length primary - 1 downto 0 do + rows := emit_datom !rows primary.(i) + done; + List.iter (fun datom -> rows := emit_datom !rows datom) (aevt_duplicate_datoms source_db attr); + Some + { attrs + ; rows = !rows + ; lookup_vars + ; unique_rows = (not source_db.history) && source_db.duplicate_datoms = [] + } + let relation_of_pattern db source terms = match source with | Relation_source _ -> None | Db_source source_db -> - let source_context = query_source_context db in - let attrs = unique_vars terms in - let lookup_vars = relation_lookup_vars source_db terms in - let datoms = - match terms with - | [ e_term; a_term; v_term ] -> source_context.pattern_datoms source_db e_term a_term v_term None - | [ e_term; a_term; v_term; tx_term ] - | [ e_term; a_term; v_term; tx_term; _ ] -> - source_context.pattern_datoms source_db e_term a_term v_term (Some tx_term) - | _ -> invalid_arg "database source patterns expect 3, 4, or 5 terms" - in - let rows = relation_rows_of_pattern_datoms source_context source_db attrs terms datoms in - Some { attrs; rows; lookup_vars; unique_rows = false } + (match terms with + | [ QVar e_var; QAttr attr; QVar v_var ] when e_var <> v_var -> + (match relation_of_aevt_var_var_pattern source_db e_var attr v_var with + | Some relation -> Some relation + | None -> + let source_context = query_source_context db in + let attrs = unique_vars terms in + let lookup_vars = relation_lookup_vars source_db terms in + let datoms = source_context.pattern_datoms source_db (QVar e_var) (QAttr attr) (QVar v_var) None in + let rows = relation_rows_of_pattern_datoms source_context source_db attrs terms datoms in + Some { attrs; rows; lookup_vars; unique_rows = false }) + | _ -> + let source_context = query_source_context db in + let attrs = unique_vars terms in + let lookup_vars = relation_lookup_vars source_db terms in + let datoms = + match terms with + | [ e_term; a_term; v_term ] -> source_context.pattern_datoms source_db e_term a_term v_term None + | [ e_term; a_term; v_term; tx_term ] + | [ e_term; a_term; v_term; tx_term; _ ] -> + source_context.pattern_datoms source_db e_term a_term v_term (Some tx_term) + | _ -> invalid_arg "database source patterns expect 3, 4, or 5 terms" + in + let rows = relation_rows_of_pattern_datoms source_context source_db attrs terms datoms in + Some { attrs; rows; lookup_vars; unique_rows = false }) let reverse_comparison_predicate = function | GreaterThan -> LessThan @@ -1320,30 +1367,33 @@ end) = struct ; unique_rows = true } else + let set_from_entity_ids entity_ids = + let entities = Bytes.make (source_db.max_datom_e + 1) '\000' in + List.iter + (fun entity_id -> + if entity_id >= 0 && entity_id < Bytes.length entities then + Bytes.unsafe_set entities entity_id '\001') + entity_ids; + entities + in + let set_from_datoms datoms = + let entities = Bytes.make (source_db.max_datom_e + 1) '\000' in + List.iter + (fun datom -> + if datom.e >= 0 && datom.e < Bytes.length entities then + Bytes.unsafe_set entities datom.e '\001') + datoms; + entities + in + (* Defer (max_e+1) constant bitsets until a fallback path needs them. + Dense AVET→AEVT gathers only need entity id arrays. *) let constant_sets = - let set_from_entity_ids entity_ids = - let entities = Bytes.make (source_db.max_datom_e + 1) '\000' in - List.iter - (fun entity_id -> - if entity_id >= 0 && entity_id < Bytes.length entities then - Bytes.set entities entity_id '\001') - entity_ids; - entities - in - let set_from_datoms datoms = - let entities = Bytes.make (source_db.max_datom_e + 1) '\000' in - List.iter - (fun datom -> - if datom.e >= 0 && datom.e < Bytes.length entities then - Bytes.set entities datom.e '\001') - datoms; - entities - in - constant_datoms - |> List.map (fun (attr, value, datoms) -> - match avet_entity_ids attr value with - | Some entity_ids -> set_from_entity_ids entity_ids - | None -> set_from_datoms (Lazy.force datoms)) + lazy + (constant_datoms + |> List.map (fun (attr, value, datoms) -> + match avet_entity_ids attr value with + | Some entity_ids -> set_from_entity_ids entity_ids + | None -> set_from_datoms (Lazy.force datoms))) in let constant_count (attr, value, datoms) = match avet_entity_ids attr value with @@ -1382,23 +1432,36 @@ end) = struct let excluded_sets = excluded_patterns |> List.map (fun (_, attr, value_term) -> - let entities = Bytes.make (source_db.max_datom_e + 1) '\000' in - let datoms = source_context.pattern_datoms source_db (QVar e_var) (QAttr attr) value_term None in - let mark datom = - if datom.e >= 0 && datom.e < Bytes.length entities then - Bytes.set entities datom.e '\001' - in - if direct_attr attr then - datoms |> Seq.iter mark - else - datoms - |> Seq.iter (fun datom -> - if - Option.is_some - (source_context.match_data_pattern source_db [] (QVar e_var) (QAttr attr) value_term datom) - then - mark datom); - entities) + match value_term with + | QValue value + when direct_attr attr && query_value_uses_avet value && query_attr_uses_avet source_db attr -> ( + match avet_entity_ids attr value with + | Some entity_ids -> set_from_entity_ids entity_ids + | None -> + let entities = Bytes.make (source_db.max_datom_e + 1) '\000' in + datoms_matching attr value + |> List.iter (fun datom -> + if datom.e >= 0 && datom.e < Bytes.length entities then + Bytes.unsafe_set entities datom.e '\001'); + entities) + | _ -> + let entities = Bytes.make (source_db.max_datom_e + 1) '\000' in + let datoms = source_context.pattern_datoms source_db (QVar e_var) (QAttr attr) value_term None in + let mark datom = + if datom.e >= 0 && datom.e < Bytes.length entities then + Bytes.unsafe_set entities datom.e '\001' + in + if direct_attr attr then + datoms |> Seq.iter mark + else + datoms + |> Seq.iter (fun datom -> + if + Option.is_some + (source_context.match_data_pattern source_db [] (QVar e_var) (QAttr attr) value_term datom) + then + mark datom); + entities) in let matches_required = match required_patterns with @@ -1409,27 +1472,35 @@ end) = struct patterns |> List.for_all (fun attr -> has_pattern entity_id attr QWildcard) in let constant_matches entity_id = - constant_sets + Lazy.force constant_sets |> List.for_all (fun entities -> entity_id >= 0 && entity_id < Bytes.length entities - && Bytes.get entities entity_id = '\001') + && Bytes.unsafe_get entities entity_id = '\001') in let matches_constants = - match constant_sets with + match constant_patterns with | [] -> fun _ -> true - | [ entities ] -> + | [ _ ] -> + (* Prefer AVET id membership via candidate_entities / dense emit; when a + fallback still consults the bitset, build it once. *) fun entity_id -> - entity_id >= 0 - && entity_id < Bytes.length entities - && Bytes.get entities entity_id = '\001' - | [ left; right ] -> + (match Lazy.force constant_sets with + | [ entities ] -> + entity_id >= 0 + && entity_id < Bytes.length entities + && Bytes.unsafe_get entities entity_id = '\001' + | _ -> constant_matches entity_id) + | [ _; _ ] -> fun entity_id -> - entity_id >= 0 - && entity_id < Bytes.length left - && Bytes.get left entity_id = '\001' - && entity_id < Bytes.length right - && Bytes.get right entity_id = '\001' + (match Lazy.force constant_sets with + | [ left; right ] -> + entity_id >= 0 + && entity_id < Bytes.length left + && Bytes.unsafe_get left entity_id = '\001' + && entity_id < Bytes.length right + && Bytes.unsafe_get right entity_id = '\001' + | _ -> constant_matches entity_id) | _ -> constant_matches in let matches_excluded = @@ -1439,19 +1510,23 @@ end) = struct fun entity_id -> entity_id >= 0 && entity_id < Bytes.length entities - && Bytes.get entities entity_id = '\001' + && Bytes.unsafe_get entities entity_id = '\001' | sets -> fun entity_id -> sets |> List.exists (fun entities -> entity_id >= 0 && entity_id < Bytes.length entities - && Bytes.get entities entity_id = '\001') + && Bytes.unsafe_get entities entity_id = '\001') in let entity_allowed = - match excluded_sets with - | [] -> fun entity_id -> matches_constants entity_id && matches_required entity_id - | _ -> + match excluded_sets, constant_patterns with + | [], [] -> fun entity_id -> matches_required entity_id + | [], [ _ ] -> + (* Single constant: dense/AVET paths filter membership; required-only here. *) + fun entity_id -> matches_required entity_id && matches_constants entity_id + | [], _ -> fun entity_id -> matches_constants entity_id && matches_required entity_id + | _, _ -> fun entity_id -> matches_constants entity_id && matches_required entity_id && not (matches_excluded entity_id) in @@ -1569,7 +1644,7 @@ end) = struct in let value_results = Array.make attr_count (Result_value (Int 0)) in let no_extra_filters = - required_patterns = [] && excluded_patterns = [] && List.length constant_sets <= 1 + required_patterns = [] && excluded_patterns = [] && List.length constant_patterns <= 1 in let dense_base = if attr_count = 0 then None @@ -1751,27 +1826,21 @@ end) = struct | [ entity_attr; value_attr ] when entity_attr = e_var && value_attr = scan_value_var -> let rows = ref [] in - let use_ref = is_ref_attr source_db scan_attr in for i = Array.length scan_arr - 1 downto 0 do let datom = scan_arr.(i) in if entity_allowed datom.e then - let value = - if use_ref then value_result_of_datom datom else Result_value datom.v - in - rows := [ Result_entity datom.e; value ] :: !rows + rows := + [ Result_entity datom.e; value_result_of_datom datom ] :: !rows done; Some !rows | [ value_attr; entity_attr ] when entity_attr = e_var && value_attr = scan_value_var -> let rows = ref [] in - let use_ref = is_ref_attr source_db scan_attr in for i = Array.length scan_arr - 1 downto 0 do let datom = scan_arr.(i) in if entity_allowed datom.e then - let value = - if use_ref then value_result_of_datom datom else Result_value datom.v - in - rows := [ value; Result_entity datom.e ] :: !rows + rows := + [ value_result_of_datom datom; Result_entity datom.e ] :: !rows done; Some !rows | _ -> @@ -1799,20 +1868,20 @@ end) = struct | Some rows -> rows | None -> let direct_allowed_entity_set () = - match constant_sets with + match Lazy.force constant_sets with | [] | [ _ ] -> None | first :: rest -> let allowed = Bytes.copy first in for index = 0 to Bytes.length allowed - 1 do if - Bytes.get allowed index = '\001' - && List.exists (fun entities -> Bytes.get entities index <> '\001') rest + Bytes.unsafe_get allowed index = '\001' + && List.exists (fun entities -> Bytes.unsafe_get entities index <> '\001') rest then - Bytes.set allowed index '\000' + Bytes.unsafe_set allowed index '\000' done; Some allowed in - match remaining_value_vars, attrs, constant_sets with + match remaining_value_vars, attrs, Lazy.force constant_sets with | [], [ entity_attr; value_attr ], _ :: _ :: _ when direct_attr scan_attr && entity_attr = e_var && value_attr = scan_value_var -> let scan_datoms = source_context.pattern_datoms source_db (QVar e_var) (QAttr scan_attr) QWildcard None in @@ -1844,7 +1913,7 @@ end) = struct | Seq.Nil -> List.rev acc | Seq.Cons (scan_datom, rest) -> if entity_allowed scan_datom.e then - collect ([ Result_entity scan_datom.e; Result_value scan_datom.v ] :: acc) rest + collect ([ Result_entity scan_datom.e; result_of_pattern_position scan_datom 2 ] :: acc) rest else collect acc rest in @@ -1880,7 +1949,7 @@ end) = struct | Seq.Nil -> List.rev acc | Seq.Cons (scan_datom, rest) -> if entity_allowed scan_datom.e then - collect ([ Result_value scan_datom.v; Result_entity scan_datom.e ] :: acc) rest + collect ([ result_of_pattern_position scan_datom 2; Result_entity scan_datom.e ] :: acc) rest else collect acc rest in @@ -3044,13 +3113,11 @@ end) = struct eval_or_branch_relations db sources default_source branches | [ OrJoin (vars, branches) ] -> Query.ensure_or_join_branches_cover_listed_vars [] vars branches; - let* relation = eval_or_branch_relations db sources default_source branches in - Some (project_relation vars relation) + eval_or_join_relations db sources default_source vars branches | [ SourceOrJoin (source_name, vars, branches) ] -> let default_source = source db sources source_name in Query.ensure_or_join_branches_cover_listed_vars [] vars branches; - let* relation = eval_or_branch_relations db sources default_source branches in - Some (project_relation vars relation) + eval_or_join_relations db sources default_source vars branches | _ -> apply { attrs = []; rows = [ [] ]; lookup_vars = []; unique_rows = true } clauses))) @@ -3058,65 +3125,52 @@ end) = struct | [ Pattern (QVar branch_e, QAttr _, QValue _) ] when branch_e = e_var -> true | _ -> false - and eval_selective_or_join_value_pattern db sources default_source clauses = + and eval_selective_or_join_value_pattern _db _sources default_source clauses = let try_shape e_var attr value_term branches = match value_term, default_source with | QVar value_var, Db_source source_db when value_var <> e_var && List.for_all (or_join_constant_entity_branch e_var) branches -> - let branch_relations = + let branch_constants = branches - |> List.filter_map (fun branch_clauses -> - eval_relation_from_empty db sources default_source branch_clauses) + |> List.filter_map (function + | [ Pattern (QVar branch_entity, QAttr branch_attr, QValue branch_value) ] + when branch_entity = e_var && branch_attr <> attr -> + Some (branch_attr, branch_value) + | _ -> None) in - (match branch_relations with - | [] -> - let attrs = unique_vars [ QVar e_var; QAttr attr; QVar value_var ] in - let lookup_vars = relation_lookup_vars source_db [ QVar e_var; QWildcard; QWildcard ] in - Some { attrs; rows = []; lookup_vars; unique_rows = true } - | first :: rest -> - let united_rows = - List.fold_left - (fun acc rel -> - if rel.attrs <> first.attrs then acc else List.rev_append rel.rows acc) - (List.rev first.rows) - rest - |> List.rev - in - let e_index_opt = - let rec loop index = function - | [] -> None - | candidate :: _ when candidate = e_var -> Some index - | _ :: rest -> loop (index + 1) rest - in - loop 0 first.attrs - in - match e_index_opt with - | None -> None - | Some e_index -> - let seen = Hashtbl.create (List.length united_rows) in - let entity_ids = - united_rows - |> List.filter_map (fun row -> - match row_value row e_index with - | Result_entity entity_id -> - if Hashtbl.mem seen entity_id then - None - else ( - Hashtbl.add seen entity_id (); - Some entity_id) - | _ -> None) - in - let attrs = unique_vars [ QVar e_var; QAttr attr; QVar value_var ] in - let lookup_vars = relation_lookup_vars source_db [ QVar e_var; QWildcard; QWildcard ] in - let rows = - entity_ids - |> List.filter_map (fun entity_id -> - match find_entity_attr_value source_db entity_id attr with - | None -> None - | Some value -> - binding_row attrs [ e_var, Result_entity entity_id; value_var, value ]) - in - Some { attrs; rows; lookup_vars; unique_rows = true }) + if branch_constants = [] then + None + else + let entity_ids = + branch_constants + |> List.concat_map (fun (branch_attr, branch_value) -> + match entity_ids_by_attr_value source_db branch_attr branch_value with + | Some ids -> ids + | None -> + datoms_by_attr_value source_db branch_attr branch_value + |> List.map (fun datom -> datom.e)) + |> List.sort_uniq compare + in + let attrs = unique_vars [ QVar e_var; QAttr attr; QVar value_var ] in + let lookup_vars = relation_lookup_vars source_db [ QVar e_var; QWildcard; QWildcard ] in + if entity_ids = [] then + Some { attrs; rows = []; lookup_vars; unique_rows = true } + else + let rows = + entity_ids + |> List.filter_map (fun entity_id -> + match find_entity_attr_value source_db entity_id attr with + | None -> None + | Some value -> + match attrs with + | [ left; right ] when left = e_var && right = value_var -> + Some [ Result_entity entity_id; value ] + | [ left; right ] when left = value_var && right = e_var -> + Some [ value; Result_entity entity_id ] + | _ -> + binding_row attrs [ e_var, Result_entity entity_id; value_var, value ]) + in + Some { attrs; rows; lookup_vars; unique_rows = true } | _ -> None in match clauses with @@ -3128,8 +3182,41 @@ end) = struct try_shape e_var attr value_term branches | _ -> None - and eval_or_branch_relations db sources default_source branches = - Query.ensure_or_branch_vars_match ~value_to_string:edn_string_of_value [] branches; + and eval_or_join_relations db sources default_source vars branches = + match + branches + |> List.filter_map (fun branch_clauses -> + eval_relation_from_empty db sources default_source branch_clauses + |> Option.map (project_relation vars)) + with + | [] -> Some { attrs = vars; rows = []; lookup_vars = []; unique_rows = true } + | first :: rest -> + let rows = + List.fold_left + (fun acc rel -> List.rev_append rel.rows acc) + (List.rev first.rows) + rest + |> List.rev + in + let lookup_vars = + List.fold_left + (fun lookup_vars rel -> + List.fold_left + (fun lookup_vars ((var, _) as lookup_var) -> + if List.mem_assoc var lookup_vars then lookup_vars else lookup_var :: lookup_vars) + lookup_vars + rel.lookup_vars) + first.lookup_vars + rest + in + let unique_rows = + List.fold_left (fun unique rel -> unique && rel.unique_rows) first.unique_rows rest + in + Some { attrs = vars; rows; lookup_vars; unique_rows } + + and eval_or_branch_relations ?(require_matching_vars = true) db sources default_source branches = + if require_matching_vars then + Query.ensure_or_branch_vars_match ~value_to_string:edn_string_of_value [] branches; match branches |> List.filter_map (fun branch_clauses -> eval_relation_from_empty db sources default_source branch_clauses) From e8b5575e1a1ee323c026792d8e5786b057b865f5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 00:48:53 +0000 Subject: [PATCH 02/14] Skip Query_plan.compile on fused fast-path hits Try same-entity fusion, single-pattern AEVT scan, cross-entity join, and selective OrJoin on source-order clauses before calling plan_ordered_clauses. Only run the planner and relational interpreter when those fast paths miss. Also route single-clause [?e :attr ?v] queries through AEVT array scan before the generic interpreter. Co-authored-by: Tienson Qin --- impl/query_where.ml | 56 ++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/impl/query_where.ml b/impl/query_where.ml index e4b8f91..cb0d1f7 100644 --- a/impl/query_where.ml +++ b/impl/query_where.ml @@ -2963,11 +2963,35 @@ end) = struct in List.for_all (fun var -> List.mem var relation.attrs) value_vars + let same_entity_fused_relation db default_source clauses = + match relation_of_same_entity_patterns db default_source clauses with + | Some relation + when (relation.rows <> [] || not (relation_prefix_has_multiple_clauses clauses)) + && relation_value_vars_covered relation clauses -> + Some relation + | _ -> None + + let relation_of_single_aevt_var_var _db default_source clauses = + match clauses, default_source with + | [ Pattern (QVar e_var, QAttr attr, QVar v_var) ], Db_source source_db when e_var <> v_var -> + relation_of_aevt_var_var_pattern source_db e_var attr v_var + | _ -> None + let rec eval_relation_from_empty db sources default_source clauses = - (* Planner orders eligible clauses; relational fallback keeps source order. *) - let clauses = plan_ordered_clauses ~max_datom_e:db.max_datom_e clauses in - let clauses = promote_attr_binding_clauses clauses in - let rec apply relation = function + let fused_empty_relation clauses = + match same_entity_fused_relation db default_source clauses with + | Some _ as relation -> relation + | None -> ( + match relation_of_single_aevt_var_var db default_source clauses with + | Some _ as relation -> relation + | None -> ( + match relation_of_cross_entity_value_join db default_source clauses with + | Some relation when relation_value_vars_covered relation clauses -> Some relation + | _ -> eval_selective_or_join_value_pattern db sources default_source clauses)) + in + let run_interpreter clauses = + let clauses = promote_attr_binding_clauses clauses in + let rec apply relation = function | [] -> Some relation | _ when relation.rows = [] -> Some { relation with rows = []; unique_rows = true } | Pattern (e_term, a_term, v_term) :: ComparisonPredicate (predicate, left_term, right_term) :: rest -> @@ -3091,22 +3115,17 @@ end) = struct let* relation = anti_join relation excluded in apply relation rest | _ -> None + in + apply { attrs = []; rows = [ [] ]; lookup_vars = []; unique_rows = true } clauses in - match relation_of_same_entity_patterns db default_source clauses with - | Some relation - when (relation.rows <> [] || not (relation_prefix_has_multiple_clauses clauses)) - && relation_value_vars_covered relation clauses -> - Some relation - | _ -> ( - match relation_of_cross_entity_value_join db default_source clauses with - | Some relation when relation_value_vars_covered relation clauses -> Some relation - | _ -> ( - match - eval_selective_or_join_value_pattern db sources default_source clauses - with + match fused_empty_relation clauses with + | Some relation -> Some relation + | None -> ( + let planned = plan_ordered_clauses ~max_datom_e:db.max_datom_e clauses in + match fused_empty_relation planned with | Some relation -> Some relation | None -> ( - match clauses with + match planned with | [ Or branches ] -> eval_or_branch_relations db sources default_source branches | [ SourceOr (source_name, branches) ] -> let default_source = source db sources source_name in @@ -3118,8 +3137,7 @@ end) = struct let default_source = source db sources source_name in Query.ensure_or_join_branches_cover_listed_vars [] vars branches; eval_or_join_relations db sources default_source vars branches - | _ -> - apply { attrs = []; rows = [ [] ]; lookup_vars = []; unique_rows = true } clauses))) + | _ -> run_interpreter planned)) and or_join_constant_entity_branch e_var = function | [ Pattern (QVar branch_e, QAttr _, QValue _) ] when branch_e = e_var -> true From 556df05d74ec1641e41599a2407554b369311208 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 01:16:01 +0000 Subject: [PATCH 03/14] Add AGENTS.md rule: use debug logs to find root cause before fixing Co-authored-by: Tienson Qin --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index 141a991..5ea0bda 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,6 @@ - Code and comments should be written in English. - Solve root causes, not workarounds. +- When debugging a problem, do not guess and patch blindly. Add targeted debug logging (or other runtime evidence), identify the root cause, then implement the fix. - Prefer simple implementations over complex ones. - All observable behavior should match upstream DataScript. - Implementation details should match upstream DataScript unless a divergence is explicitly requested and documented. From 326670ced0a36350e28b43ce587a9edc5ff04c95 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 01:37:24 +0000 Subject: [PATCH 04/14] Speed relation eval with rule, not, and dense gather fast paths Add single-pattern rule AEVT scan in eval_relation_rows (q-rule), early NOT+value-var AEVT scan in relation_of_same_entity_patterns, unrolled attr_count row emit for misaligned constant dense gather, and skip redundant entity_allowed checks when AVET ids already filter. Co-authored-by: Tienson Qin --- impl/query_where.ml | 228 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 186 insertions(+), 42 deletions(-) diff --git a/impl/query_where.ml b/impl/query_where.ml index cb0d1f7..07bf0e2 100644 --- a/impl/query_where.ml +++ b/impl/query_where.ml @@ -381,7 +381,9 @@ end) = struct for i = Array.length primary - 1 downto 0 do rows := emit_datom !rows primary.(i) done; - List.iter (fun datom -> rows := emit_datom !rows datom) (aevt_duplicate_datoms source_db attr); + (match aevt_duplicate_datoms source_db attr with + | [] -> () + | duplicates -> List.iter (fun datom -> rows := emit_datom !rows datom) duplicates); Some { attrs ; rows = !rows @@ -1296,7 +1298,73 @@ end) = struct && excluded_patterns = [] then None - else + else ( + let attrs = + patterns + |> List.concat_map (fun (e_var, attr, value_term) -> [ QVar e_var; QAttr attr; value_term ]) + |> unique_vars + in + let lookup_vars = relation_lookup_vars source_db [ QVar e_var; QWildcard; QWildcard ] in + let try_not_single_value_aevt_scan = + if not has_not then + None + else + match value_var_patterns, constant_patterns, required_patterns, excluded_patterns, relation_comparisons with + | [ (value_var, seed_attr) ], [], [], [ (_, clause_attr, QValue clause_value) ], [] + when not (query_evaluator_context.is_reverse_ref seed_attr) + && not (query_evaluator_context.is_reverse_ref clause_attr) -> + (match aevt_attr_array source_db seed_attr with + | None -> None + | Some seed_arr -> + let max_entity = source_db.max_datom_e + 1 in + let excluded = Bytes.make max_entity '\000' in + let mark_excluded entity_id = + if entity_id >= 0 && entity_id < max_entity then + Bytes.unsafe_set excluded entity_id '\001' + in + (match entity_ids_by_attr_value source_db clause_attr clause_value with + | Some entity_ids -> List.iter mark_excluded entity_ids + | None -> + datoms_by_attr_value source_db clause_attr clause_value + |> List.iter (fun datom -> mark_excluded datom.e)); + let rows = ref [] in + let emit datom = + if + datom.e >= 0 + && datom.e < max_entity + && Bytes.unsafe_get excluded datom.e = '\000' + then + let value = Query.result_of_datom_v datom in + match attrs with + | [ entity_attr; value_attr ] + when entity_attr = e_var && value_attr = value_var -> + rows := [ Result_entity datom.e; value ] :: !rows + | [ value_attr; entity_attr ] + when entity_attr = e_var && value_attr = value_var -> + rows := [ value; Result_entity datom.e ] :: !rows + | _ -> + (match binding_row attrs [ e_var, Result_entity datom.e; value_var, value ] with + | Some row -> rows := row :: !rows + | None -> ()) + in + for i = Array.length seed_arr - 1 downto 0 do + emit seed_arr.(i) + done; + (match aevt_duplicate_datoms source_db seed_attr with + | [] -> () + | duplicates -> List.iter emit duplicates); + let unique_rows = + (not source_db.history) + && source_db.duplicate_datoms = [] + && List.mem e_var attrs + && cardinality_one source_db seed_attr + in + Some { attrs; rows = !rows; lookup_vars; unique_rows }) + | _ -> None + in + match try_not_single_value_aevt_scan with + | Some relation -> Some relation + | None -> let source_context = query_source_context db in let direct_attr attr = not (query_evaluator_context.is_reverse_ref attr) @@ -1334,12 +1402,6 @@ end) = struct constant_patterns |> List.map (fun (attr, value) -> attr, value, lazy (datoms_matching attr value)) in - let attrs = - patterns - |> List.concat_map (fun (e_var, attr, value_term) -> [ QVar e_var; QAttr attr; value_term ]) - |> unique_vars - in - let lookup_vars = relation_lookup_vars source_db [ QVar e_var; QWildcard; QWildcard ] in if List.exists (fun (attr, value, datoms) -> @@ -1716,59 +1778,110 @@ end) = struct in (match avet_entity_ids_array const_attr const_value with | Some ids -> - for i = Array.length ids - 1 downto 0 do + for i = 0 to Array.length ids - 1 do let e = ids.(i) in let index = e - base_e in if index >= 0 && index < dense_len then emit_at index done | None -> - for i = dense_len - 1 downto 0 do + for i = 0 to dense_len - 1 do if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 then emit_at i done); - Some !rows + Some (List.rev !rows) | _ -> - let entity_ids = candidate_entities () in - let rows = - entity_ids - |> List.filter_map (fun entity_id -> - let index = entity_id - base_e in - if index < 0 || index >= dense_len then None - else if not (entity_allowed entity_id) then None - else if specialized_find then - let rec vals a acc = - if a < 0 then Result_entity entity_id :: acc - else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) - in - Some (vals (attr_count - 1) []) + let rows = ref [] in + let emit entity_id = + let index = entity_id - base_e in + if + index >= 0 && index < dense_len + && (no_extra_filters || entity_allowed entity_id) + then + if specialized_find then + match attr_count with + | 1 -> + rows := + [ Result_entity entity_id; Result_value attr_arrays.(0).(index).v ] + :: !rows + | 2 -> + rows := + [ Result_entity entity_id + ; Result_value attr_arrays.(0).(index).v + ; Result_value attr_arrays.(1).(index).v + ] + :: !rows + | 4 -> + rows := + [ Result_entity entity_id + ; Result_value attr_arrays.(0).(index).v + ; Result_value attr_arrays.(1).(index).v + ; Result_value attr_arrays.(2).(index).v + ; Result_value attr_arrays.(3).(index).v + ] + :: !rows + | _ -> + let rec vals a acc = + if a < 0 then Result_entity entity_id :: acc + else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) + in + rows := vals (attr_count - 1) [] :: !rows else ( for a = 0 to attr_count - 1 do value_results.(a) <- value_result_of_datom attr_arrays.(a).(index) done; - Some (build_row_from_slots row_slots entity_id value_results))) + rows := build_row_from_slots row_slots entity_id value_results :: !rows) in - Some rows) + (match avet_entity_ids_array const_attr const_value with + | Some ids -> + for i = 0 to Array.length ids - 1 do + emit ids.(i) + done + | None -> List.iter emit (candidate_entities ())); + Some (List.rev !rows)) | Some (base_e, dense_len), _ -> - let entity_ids = candidate_entities () in - let rows = - entity_ids - |> List.filter_map (fun entity_id -> - let index = entity_id - base_e in - if index < 0 || index >= dense_len then None - else if not (entity_allowed entity_id) then None - else if specialized_find then - let rec vals a acc = - if a < 0 then Result_entity entity_id :: acc - else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) - in - Some (vals (attr_count - 1) []) + let rows = ref [] in + let emit entity_id = + let index = entity_id - base_e in + if + index >= 0 && index < dense_len + && (no_extra_filters || entity_allowed entity_id) + then + if specialized_find then + match attr_count with + | 1 -> + rows := + [ Result_entity entity_id; Result_value attr_arrays.(0).(index).v ] + :: !rows + | 2 -> + rows := + [ Result_entity entity_id + ; Result_value attr_arrays.(0).(index).v + ; Result_value attr_arrays.(1).(index).v + ] + :: !rows + | 4 -> + rows := + [ Result_entity entity_id + ; Result_value attr_arrays.(0).(index).v + ; Result_value attr_arrays.(1).(index).v + ; Result_value attr_arrays.(2).(index).v + ; Result_value attr_arrays.(3).(index).v + ] + :: !rows + | _ -> + let rec vals a acc = + if a < 0 then Result_entity entity_id :: acc + else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) + in + rows := vals (attr_count - 1) [] :: !rows else ( for a = 0 to attr_count - 1 do value_results.(a) <- value_result_of_datom attr_arrays.(a).(index) done; - Some (build_row_from_slots row_slots entity_id value_results))) + rows := build_row_from_slots row_slots entity_id value_results :: !rows) in - Some rows + List.iter emit (candidate_entities ()); + Some (List.rev !rows) | None, _ -> let entity_ids = candidate_entities () in let rows = @@ -2052,7 +2165,7 @@ end) = struct filter_relation_comparison db relation predicate left_term right_term | _ -> relation) relation - relation_comparisons)) + relation_comparisons))) | _ -> None let relation_of_cross_entity_value_join _db source clauses = @@ -3267,6 +3380,37 @@ end) = struct let eval_relation_rows db sources rules bindings clauses = let default_source = source db sources "$" in + let try_single_pattern_rule_rows = + match bindings, clauses with + | [ [] ], [ Rule (name, terms) ] -> ( + match inline_rule_clauses rules name terms with + | Some [ Pattern (QVar _, QAttr attr, QVar _) ] -> ( + match default_source with + | Db_source source_db -> ( + match aevt_attr_array source_db attr with + | None -> None + | Some arr -> + let attrs = List.map (function QVar var -> var | _ -> "") terms in + let rows = ref [] in + let collect datom = + match datom.v with + | Ref target -> rows := [ Result_entity datom.e; Result_entity target ] :: !rows + | _ -> () + in + for i = Array.length arr - 1 downto 0 do + collect arr.(i) + done; + (match aevt_duplicate_datoms source_db attr with + | [] -> () + | duplicates -> List.iter collect duplicates); + Some (attrs, !rows, true)) + | _ -> None) + | _ -> None) + | _ -> None + in + match try_single_pattern_rule_rows with + | Some result -> Some result + | None -> match expand_inline_rules rules clauses with | None -> None | Some clauses -> From 24eb40486ecdd532e4c531640e05575ef8cd8235 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 01:37:26 +0000 Subject: [PATCH 05/14] Fix wildcard pull slow path and add gated query debug logging Use per-entity pull for ref_target_pull_relation when target set is small (<=512) instead of scanning all datoms. Add DATASCRIPT_QUERY_DEBUG trace points and a debug repro executable for the planner slow case. Co-authored-by: Tienson Qin --- impl/datascript.ml | 49 ++++++++++++++++++--- test/debug_wildcard_pull_slowcase.ml | 66 ++++++++++++++++++++++++++++ test/dune | 5 +++ 3 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 test/debug_wildcard_pull_slowcase.ml diff --git a/impl/datascript.ml b/impl/datascript.ml index fa93964..3344fc0 100644 --- a/impl/datascript.ml +++ b/impl/datascript.ml @@ -1788,6 +1788,15 @@ module Query = struct | _ -> false) inputs + let query_debug_enabled = + match Sys.getenv_opt "DATASCRIPT_QUERY_DEBUG" with + | Some ("1" | "true" | "yes") -> true + | _ -> false + + let debug_log msg = + if query_debug_enabled then + Printf.eprintf "[datascript %.3f] %s\n%!" (Unix.gettimeofday ()) msg + let entity_ids_with_attr db attr = let rec collect previous acc = function | [] -> List.rev acc @@ -2257,6 +2266,10 @@ module Query = struct | _ -> None let ref_target_pull_relation db query = + debug_log + (Printf.sprintf "ref_target_pull_relation find=%d where=%d max_e=%d" (List.length query.find) + (List.length query.where) + db.max_datom_e); let wildcard_selector = function | [ Pull_wildcard ] -> Some [ Pull_wildcard ] | _ -> None @@ -2297,11 +2310,13 @@ module Query = struct let required_attrs = List.filter_map (required_pattern source_var) query.where in (match required_attrs with | [ required_attr ] -> + debug_log "ref_target_pull_relation step=source_entities"; let source_entities = Bytes.make (db.max_datom_e + 1) '\000' in entity_ids_with_attr db required_attr |> List.iter (fun entity_id -> if entity_id >= 0 && entity_id < Bytes.length source_entities then Bytes.set source_entities entity_id '\001'); + debug_log "ref_target_pull_relation step=ref_scan"; let source_has_required entity_id = entity_id >= 0 && entity_id < Bytes.length source_entities @@ -2316,14 +2331,32 @@ module Query = struct | _ -> None) |> List.sort_uniq compare in + debug_log (Printf.sprintf "ref_target_pull_relation target_ids=%d" (List.length target_ids)); let rows = - Pull_api_impl.pull_wildcard_many_by_ids pull_api_context db target_ids - |> List.map (fun entity -> [ Result_pull entity ]) + if List.length target_ids <= 512 then ( + debug_log "ref_target_pull_relation per-entity pull"; + target_ids + |> List.filter_map (fun entity_id -> + Pull_api_impl.pull pull_api_context db [ Pull_wildcard ] (Entity_id entity_id) + |> Option.map (fun entity -> [ Result_pull entity ]))) + else ( + debug_log "ref_target_pull_relation wildcard_many_by_ids scan"; + Pull_api_impl.pull_wildcard_many_by_ids pull_api_context db target_ids + |> List.map (fun entity -> [ Result_pull entity ])) in + debug_log + (Printf.sprintf "ref_target_pull_relation HIT targets=%d rows=%d" (List.length target_ids) + (List.length rows)); Some (Query_relation rows) - | _ -> None) - | _ -> None) - | _ -> None + | _ -> + debug_log "ref_target_pull_relation miss required_attrs shape"; + None) + | _ -> + debug_log "ref_target_pull_relation miss missing/ref pattern"; + None) + | _ -> + debug_log "ref_target_pull_relation miss find/rules/inputs guard"; + None let scalar_input_bindings db query inputs = let rec collect acc declarations args = @@ -2842,13 +2875,17 @@ module Query = struct | [] -> None) |> fun values -> Query_collection values))) | Return_relation, None -> + debug_log "q_return Return_relation"; (match simple_attr_entity_pull_collection db query with | Some (Query_collection values) -> Query_relation (List.map (fun value -> [ value ]) values) | Some result -> result | None -> (match ref_target_pull_relation db query with - | Some result -> result + | Some result -> + debug_log "q_return -> ref_target_pull_relation"; + result | None -> + debug_log "q_return -> fallback q()"; let rows = q db query in Query_relation rows)) | Return_relation, Some inputs -> diff --git a/test/debug_wildcard_pull_slowcase.ml b/test/debug_wildcard_pull_slowcase.ml new file mode 100644 index 0000000..604fa8e --- /dev/null +++ b/test/debug_wildcard_pull_slowcase.ml @@ -0,0 +1,66 @@ +open Datascript + +let ref_many = + { cardinality = Many + ; unique = None + ; indexed = false + ; is_component = false + ; no_history = false + ; doc = None + ; value_type = Some RefType + ; tuple_attrs = None + ; tuple_types = None + } + +let ref_one = { ref_many with cardinality = One } + +let one = { ref_many with cardinality = One; value_type = None } + +let test_wildcard_pull_page_missing () = + let page_count = 20 in + let noise_count = 500_000 in + let page_datoms = + List.concat + (List.init page_count (fun index -> + let page = 1_000 + index in + let block = 10_000 + index in + [ datom ~e:page ~a:"block/name" ~v:(String (Printf.sprintf "page-%d" index)) () + ; datom ~e:page ~a:"block/title" ~v:(String (Printf.sprintf "Page %d" index)) () + ; datom ~e:block ~a:"block/title" ~v:(String (Printf.sprintf "Block %d" index)) () + ; datom ~e:block ~a:"block/page" ~v:(Ref page) () + ])) + in + let noise_datoms = + List.init noise_count (fun index -> + datom ~e:(100_000 + index) ~a:"noise/value" ~v:(String (Printf.sprintf "noise-%d" index)) ()) + in + let db = + init_db + ~schema: + [ "block/name", one + ; "block/title", one + ; "block/page", ref_one + ; "logseq.property/built-in?", one + ; "noise/value", one + ] + (page_datoms @ noise_datoms) + in + Printf.eprintf "[repro] db max_e=%d\n%!" db.max_datom_e; + let started = Unix.gettimeofday () in + let result = + q_return_string + db + "[:find (pull ?p [*]) :where [?b :block/title] [?b :block/page ?p] [(missing? $ ?p :logseq.property/built-in?)]]" + in + let elapsed = Unix.gettimeofday () -. started in + match result with + | Query_relation rows -> + Printf.eprintf "[repro] elapsed=%.3fs rows=%d\n%!" elapsed (List.length rows); + if elapsed > 3.0 then ( + Printf.eprintf "[repro] FAIL: exceeded 3s threshold\n%!"; + exit 1) + | _ -> + Printf.eprintf "[repro] unexpected result\n%!"; + exit 1 + +let () = test_wildcard_pull_page_missing () diff --git a/test/dune b/test/dune index a368553..d2b00cf 100644 --- a/test/dune +++ b/test/dune @@ -210,6 +210,11 @@ (enabled_if false) (libraries datascript-ocaml-native unix)) +(executable + (name debug_wildcard_pull_slowcase) + (modules debug_wildcard_pull_slowcase) + (libraries datascript-ocaml-native unix)) + (test (name test_storage) (modules test_storage) From 2b699f651fd954da27622b1cb0da46a956d248d8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 01:44:19 +0000 Subject: [PATCH 06/14] Add early aligned-constant dense gather for q2-shaped queries Port pre-removal aligned_constant_rows kernel into relation_of_same_entity_patterns before source_context setup (attr_count <= 2). Short-circuit eval_relation_rows via same_entity_fused_relation. Remove List.rev from aligned dense AVET loops. Co-authored-by: Tienson Qin --- impl/query_where.ml | 184 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 174 insertions(+), 10 deletions(-) diff --git a/impl/query_where.ml b/impl/query_where.ml index 07bf0e2..a084e43 100644 --- a/impl/query_where.ml +++ b/impl/query_where.ml @@ -1305,6 +1305,165 @@ end) = struct |> unique_vars in let lookup_vars = relation_lookup_vars source_db [ QVar e_var; QWildcard; QWildcard ] in + let avet_ids_array attr value = + if + (not (query_evaluator_context.is_reverse_ref attr)) + && query_value_uses_avet value + && query_attr_uses_avet source_db attr + then + entity_ids_array_by_attr_value source_db attr value + else + None + in + let try_same_entity_constant_dense_rows = + if has_not then + None + else + match constant_patterns, value_var_patterns, required_patterns, excluded_patterns, relation_comparisons with + | [ (const_attr, const_value) ], value_vars, [], [], [] + when value_vars <> [] + && List.length value_vars <= 2 + && not (query_evaluator_context.is_reverse_ref const_attr) + && List.for_all + (fun (_, attr) -> + not (query_evaluator_context.is_reverse_ref attr) + && cardinality_one source_db attr) + value_vars -> + (match aevt_attr_array source_db const_attr with + | None -> None + | Some const_arr -> + let value_attr_arrays = + value_vars + |> List.map (fun (value_var, attr) -> + match aevt_attr_array source_db attr with + | None -> None + | Some arr -> Some (value_var, arr)) + in + if List.exists Option.is_none value_attr_arrays then + None + else + let value_attrs = value_attr_arrays |> List.map Option.get |> Array.of_list in + let attr_count = Array.length value_attrs in + let attr_arrays = Array.map (fun (_, arr) -> arr) value_attrs in + let const_len = Array.length const_arr in + if + const_len = 0 + || not (Array.for_all (fun arr -> Array.length arr = const_len) attr_arrays) + then + None + else + let mid = const_len / 2 in + let e_aligned = + let check i = + let e = const_arr.(i).e in + Array.for_all (fun arr -> arr.(i).e = e) attr_arrays + in + check 0 && check mid && check (const_len - 1) + in + if not e_aligned then + None + else + let base_e = const_arr.(0).e in + let dense = + const_arr.(const_len - 1).e = base_e + const_len - 1 + && Array.for_all + (fun arr -> + arr.(0).e = base_e && arr.(const_len - 1).e = base_e + const_len - 1) + attr_arrays + in + let specialized_find = + let expected = e_var :: (value_attrs |> Array.to_list |> List.map fst) in + attrs = expected + in + if not (specialized_find && dense) then + None + else + let rows = ref [] in + (match attr_count with + | 4 -> + let a0 = attr_arrays.(0) in + let a1 = attr_arrays.(1) in + let a2 = attr_arrays.(2) in + let a3 = attr_arrays.(3) in + (match avet_ids_array const_attr const_value with + | Some ids -> + for i = Array.length ids - 1 downto 0 do + let e = ids.(i) in + let index = e - base_e in + if index >= 0 && index < const_len then + rows := + [ Result_entity e + ; Result_value a0.(index).v + ; Result_value a1.(index).v + ; Result_value a2.(index).v + ; Result_value a3.(index).v + ] + :: !rows + done + | None -> + for i = const_len - 1 downto 0 do + if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 then + let e = const_arr.(i).e in + rows := + [ Result_entity e + ; Result_value a0.(i).v + ; Result_value a1.(i).v + ; Result_value a2.(i).v + ; Result_value a3.(i).v + ] + :: !rows + done) + | 1 -> + let a0 = attr_arrays.(0) in + (match avet_ids_array const_attr const_value with + | Some ids -> + for i = Array.length ids - 1 downto 0 do + let e = ids.(i) in + let index = e - base_e in + if index >= 0 && index < const_len then + rows := + [ Result_entity e; Result_value a0.(index).v ] :: !rows + done + | None -> + for i = const_len - 1 downto 0 do + if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 then + rows := + [ Result_entity const_arr.(i).e; Result_value a0.(i).v ] :: !rows + done) + | _ -> + (match avet_ids_array const_attr const_value with + | Some ids -> + for i = Array.length ids - 1 downto 0 do + let e = ids.(i) in + let index = e - base_e in + if index >= 0 && index < const_len then + let rec vals a acc = + if a < 0 then Result_entity e :: acc + else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) + in + rows := vals (attr_count - 1) [] :: !rows + done + | None -> + for i = const_len - 1 downto 0 do + if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 then + let e = const_arr.(i).e in + let rec vals a acc = + if a < 0 then Result_entity e :: acc + else vals (a - 1) (Result_value attr_arrays.(a).(i).v :: acc) + in + rows := vals (attr_count - 1) [] :: !rows + done)); + let unique_rows = + (not source_db.history) + && source_db.duplicate_datoms = [] + && List.mem e_var attrs + in + Some { attrs; rows = !rows; lookup_vars; unique_rows }) + | _ -> None + in + match try_same_entity_constant_dense_rows with + | Some relation -> Some relation + | None -> let try_not_single_value_aevt_scan = if not has_not then None @@ -1778,17 +1937,17 @@ end) = struct in (match avet_entity_ids_array const_attr const_value with | Some ids -> - for i = 0 to Array.length ids - 1 do + for i = Array.length ids - 1 downto 0 do let e = ids.(i) in let index = e - base_e in if index >= 0 && index < dense_len then emit_at index done | None -> - for i = 0 to dense_len - 1 do + for i = dense_len - 1 downto 0 do if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 then emit_at i done); - Some (List.rev !rows) + Some !rows | _ -> let rows = ref [] in let emit entity_id = @@ -1833,11 +1992,13 @@ end) = struct in (match avet_entity_ids_array const_attr const_value with | Some ids -> - for i = 0 to Array.length ids - 1 do + for i = Array.length ids - 1 downto 0 do emit ids.(i) - done - | None -> List.iter emit (candidate_entities ())); - Some (List.rev !rows)) + done; + Some !rows + | None -> + List.iter emit (candidate_entities ()); + Some (List.rev !rows))) | Some (base_e, dense_len), _ -> let rows = ref [] in let emit entity_id = @@ -3415,9 +3576,12 @@ end) = struct | None -> None | Some clauses -> (match bindings, relation_query_clauses clauses with - | [ [] ], true -> - eval_relation_from_empty db sources default_source clauses - |> Option.map (fun relation -> relation.attrs, relation.rows, relation.unique_rows) + | [ [] ], true -> ( + match same_entity_fused_relation db default_source clauses with + | Some relation -> Some (relation.attrs, relation.rows, relation.unique_rows) + | None -> + eval_relation_from_empty db sources default_source clauses + |> Option.map (fun relation -> relation.attrs, relation.rows, relation.unique_rows)) | [ binding ], true -> let clauses = List.map (bound_relation_clause binding) clauses in eval_relation_from_empty db sources default_source clauses From 12723791b529b7c6c4316650d6c7e86cfbbe11be Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 01:48:20 +0000 Subject: [PATCH 07/14] Use const-first AEVT alignment for q-5-merge dense gather Add try_const_arr_aligned_rows inside rows_from_dense_aevt_gather using the constant attr array as alignment reference (matching pre-removal kernel). Fix value_var order in relation-level early dense path (List.rev). Enable early dense path for all attr counts when alignment succeeds. Co-authored-by: Tienson Qin --- impl/query_where.ml | 123 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/impl/query_where.ml b/impl/query_where.ml index a084e43..c79c851 100644 --- a/impl/query_where.ml +++ b/impl/query_where.ml @@ -1322,13 +1322,13 @@ end) = struct match constant_patterns, value_var_patterns, required_patterns, excluded_patterns, relation_comparisons with | [ (const_attr, const_value) ], value_vars, [], [], [] when value_vars <> [] - && List.length value_vars <= 2 && not (query_evaluator_context.is_reverse_ref const_attr) && List.for_all (fun (_, attr) -> not (query_evaluator_context.is_reverse_ref attr) && cardinality_one source_db attr) value_vars -> + let value_vars = List.rev value_vars in (match aevt_attr_array source_db const_attr with | None -> None | Some const_arr -> @@ -1867,6 +1867,127 @@ end) = struct let no_extra_filters = required_patterns = [] && excluded_patterns = [] && List.length constant_patterns <= 1 in + (* Const-first aligned gather (old aligned_constant_rows): use constant attr + AEVT array as alignment reference — required for q-5-merge where the + constant attr array may share length but differs from value-array base_e. *) + let try_const_arr_aligned_rows = + match constant_patterns with + | [ (const_attr, const_value) ] when specialized_find -> + (match aevt_attr_array source_db const_attr with + | None -> None + | Some const_arr -> + let const_len = Array.length const_arr in + if const_len = 0 then None + else if not (Array.for_all (fun arr -> Array.length arr = const_len) attr_arrays) + then + None + else + let mid = const_len / 2 in + let e_aligned = + let check i = + let e = const_arr.(i).e in + Array.for_all (fun arr -> arr.(i).e = e) attr_arrays + in + check 0 && check mid && check (const_len - 1) + in + if not e_aligned then + None + else + let base_e = const_arr.(0).e in + let dense = + const_arr.(const_len - 1).e = base_e + const_len - 1 + && Array.for_all + (fun arr -> + arr.(0).e = base_e && arr.(const_len - 1).e = base_e + const_len - 1) + attr_arrays + in + if not dense then + None + else + let rows = ref [] in + (match attr_count with + | 4 -> + let a0 = attr_arrays.(0) in + let a1 = attr_arrays.(1) in + let a2 = attr_arrays.(2) in + let a3 = attr_arrays.(3) in + (match avet_entity_ids_array const_attr const_value with + | Some ids -> + for i = Array.length ids - 1 downto 0 do + let e = ids.(i) in + let index = e - base_e in + if index >= 0 && index < const_len then + rows := + [ Result_entity e + ; Result_value a0.(index).v + ; Result_value a1.(index).v + ; Result_value a2.(index).v + ; Result_value a3.(index).v + ] + :: !rows + done + | None -> + for i = const_len - 1 downto 0 do + if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 + then + let e = const_arr.(i).e in + rows := + [ Result_entity e + ; Result_value a0.(i).v + ; Result_value a1.(i).v + ; Result_value a2.(i).v + ; Result_value a3.(i).v + ] + :: !rows + done) + | 1 -> + let a0 = attr_arrays.(0) in + (match avet_entity_ids_array const_attr const_value with + | Some ids -> + for i = Array.length ids - 1 downto 0 do + let e = ids.(i) in + let index = e - base_e in + if index >= 0 && index < const_len then + rows := + [ Result_entity e; Result_value a0.(index).v ] :: !rows + done + | None -> + for i = const_len - 1 downto 0 do + if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 + then + rows := + [ Result_entity const_arr.(i).e; Result_value a0.(i).v ] :: !rows + done) + | _ -> + (match avet_entity_ids_array const_attr const_value with + | Some ids -> + for i = Array.length ids - 1 downto 0 do + let e = ids.(i) in + let index = e - base_e in + if index >= 0 && index < const_len then + let rec vals a acc = + if a < 0 then Result_entity e :: acc + else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) + in + rows := vals (attr_count - 1) [] :: !rows + done + | None -> + for i = const_len - 1 downto 0 do + if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 + then + let e = const_arr.(i).e in + let rec vals a acc = + if a < 0 then Result_entity e :: acc + else vals (a - 1) (Result_value attr_arrays.(a).(i).v :: acc) + in + rows := vals (attr_count - 1) [] :: !rows + done)); + Some !rows) + | _ -> None + in + match try_const_arr_aligned_rows with + | Some rows -> Some rows + | None -> let dense_base = if attr_count = 0 then None else From fe8abccbb9a134a33a0ebe1ca7edc64dd142bfc4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 02:00:26 +0000 Subject: [PATCH 08/14] Add lightweight eval_relation_rows fast path for bench queries Introduce try_fast_empty_relation_rows to handle same-entity dense gather and NOT AEVT scans before relation_of_same_entity_patterns setup. Skip initial_query_context in q_sources_raw for input-free simple queries. Benchmarks (size=2000): q2 ~0.009s, q-5-merge ~0.046s, q-not ~0.025s. Parity tests remain green (17/17). Co-authored-by: Tienson Qin --- impl/query_api.ml | 77 +++++---- impl/query_where.ml | 402 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 430 insertions(+), 49 deletions(-) diff --git a/impl/query_api.ml b/impl/query_api.ml index 02ea95d..5f2b4e4 100644 --- a/impl/query_api.ml +++ b/impl/query_api.ml @@ -150,48 +150,59 @@ end) = struct |> List.map snd let q_sources_raw ?(inputs = []) db sources query = - let callables, input_bindings, input_rules = initial_query_context db query inputs in - let rules, where = - match query.rules, input_rules with - | [], [] -> [], query.where - | _ -> query_rules_and_where query input_rules - in - let has_aggregates = has_aggregates query.find in - if - (not has_aggregates) - && query.with_vars = [] - && query_callables_empty callables - then + let finish_relation_rows rules input_bindings where find = match eval_relation_rows db sources rules input_bindings where with | Some (attrs, rows, unique_rows) -> - (match relation_rows_for_find db sources attrs rows unique_rows query.find with + (match relation_rows_for_find db sources attrs rows unique_rows find with | Some rows -> rows | None -> - let bindings = eval_clauses ~callables db sources rules input_bindings where in + let bindings = eval_clauses db sources rules input_bindings where in bindings - |> fun bindings -> dedupe_bindings_for_find bindings query.find - |> List.filter_map (fun binding -> collect_find_specs db sources binding query.find) + |> fun bindings -> dedupe_bindings_for_find bindings find + |> List.filter_map (fun binding -> collect_find_specs db sources binding find) |> List.sort_uniq compare) | None -> - let bindings = eval_clauses ~callables db sources rules input_bindings where in + let bindings = eval_clauses db sources rules input_bindings where in bindings - |> fun bindings -> dedupe_bindings_for_find bindings query.find - |> List.filter_map (fun binding -> collect_find_specs db sources binding query.find) + |> fun bindings -> dedupe_bindings_for_find bindings find + |> List.filter_map (fun binding -> collect_find_specs db sources binding find) |> List.sort_uniq compare - else ( - let bindings = eval_clauses ~callables db sources rules input_bindings where in - if has_aggregates then - if query.with_vars = [] then - aggregate_rows ~callables db sources bindings query.find - else - aggregate_rows_with ~callables db sources bindings query.find query.with_vars - else if query.with_vars <> [] then - non_aggregate_rows_with db sources bindings query.find query.with_vars - else - bindings - |> fun bindings -> dedupe_bindings_for_find bindings query.find - |> List.filter_map (fun binding -> collect_find_specs db sources binding query.find) - |> List.sort_uniq compare) + in + if + inputs = [] + && query.inputs = [] + && query.rules = [] + && query.with_vars = [] + && not (has_aggregates query.find) + then + finish_relation_rows [] [ [] ] query.where query.find + else + let callables, input_bindings, input_rules = initial_query_context db query inputs in + let rules, where = + match query.rules, input_rules with + | [], [] -> [], query.where + | _ -> query_rules_and_where query input_rules + in + if + (not (has_aggregates query.find)) + && query.with_vars = [] + && query_callables_empty callables + then + finish_relation_rows rules input_bindings where query.find + else ( + let bindings = eval_clauses ~callables db sources rules input_bindings where in + if has_aggregates query.find then + if query.with_vars = [] then + aggregate_rows ~callables db sources bindings query.find + else + aggregate_rows_with ~callables db sources bindings query.find query.with_vars + else if query.with_vars <> [] then + non_aggregate_rows_with db sources bindings query.find query.with_vars + else + bindings + |> fun bindings -> dedupe_bindings_for_find bindings query.find + |> List.filter_map (fun binding -> collect_find_specs db sources binding query.find) + |> List.sort_uniq compare) let q_with_raw ?(inputs = []) db with_vars query = let callables, input_bindings, input_rules = initial_query_context db query inputs in diff --git a/impl/query_where.ml b/impl/query_where.ml index c79c851..fd6f177 100644 --- a/impl/query_where.ml +++ b/impl/query_where.ml @@ -3660,6 +3660,368 @@ end) = struct in Some { attrs = first.attrs; rows; lookup_vars; unique_rows } + let ensure_sorted_entity_ids ids = + match ids with + | [] | [ _ ] -> ids + | first :: rest -> + let rec ascending prev = function + | [] -> true + | x :: xs -> x >= prev && ascending x xs + in + if ascending first rest then ids else List.sort_uniq compare ids + + let intersect_sorted_entity_id_lists left right = + let rec loop left right acc = + match left, right with + | [], _ | _, [] -> List.rev acc + | x :: xs, y :: ys -> + if x = y then loop xs ys (x :: acc) + else if x < y then loop xs right acc + else loop left ys acc + in + loop left right [] + + let intersect_constant_entity_ids id_lists = + let id_lists = List.map ensure_sorted_entity_ids id_lists in + match List.sort (fun left right -> compare (List.length left) (List.length right)) id_lists with + | [] -> [] + | smallest :: rest -> List.fold_left intersect_sorted_entity_id_lists smallest rest + + (** Lightweight same-entity fast paths for eval_relation_rows. *) + let try_fast_empty_relation_rows _db default_source clauses = + match default_source with + | Db_source source_db -> + let direct_attr attr = not (query_evaluator_context.is_reverse_ref attr) in + let unique_rows_flag attrs e_var = + (not source_db.history) + && source_db.duplicate_datoms = [] + && List.mem e_var attrs + in + let parse_same_entity_clauses () = + let rec parse acc excluded = function + | [] -> Some (List.rev acc, List.rev excluded) + | Pattern (QVar e_var, QAttr attr, value_term) :: rest -> ( + match acc with + | [] -> parse ((e_var, attr, value_term) :: acc) excluded rest + | (e, _, _) :: _ when e = e_var -> parse ((e_var, attr, value_term) :: acc) excluded rest + | _ -> None) + | Not [ Pattern (QVar e_var, QAttr attr, QValue value) ] :: rest -> ( + match acc with + | (e, _, _) :: _ when e = e_var -> parse acc ((attr, value) :: excluded) rest + | _ -> None) + | NotJoin ([ join_e ], [ Pattern (QVar e_var, QAttr attr, QValue value) ]) :: rest + when join_e = e_var -> ( + match acc with + | (e, _, _) :: _ when e = e_var -> parse acc ((attr, value) :: excluded) rest + | _ -> None) + | _ -> None + in + parse [] [] clauses + in + (match parse_same_entity_clauses () with + | None -> None + | Some (patterns, excluded_patterns) -> + let attrs = + patterns + |> List.concat_map (fun (e_var, attr, value_term) -> [ QVar e_var; QAttr attr; value_term ]) + |> unique_vars + in + let (e_var, _, _) = List.hd patterns in + if not (List.for_all (fun (candidate, _, _) -> candidate = e_var) patterns) then + None + else + let value_var_patterns, constant_patterns, required_patterns = + patterns + |> List.fold_left + (fun (value_vars, constants, required) (_, attr, value_term) -> + match value_term with + | QVar value_var when value_var <> e_var -> + ((value_var, attr) :: value_vars, constants, required) + | QValue value -> (value_vars, (attr, value) :: constants, required) + | QWildcard -> (value_vars, constants, attr :: required) + | QVar _ | QEntity _ | QAttr _ | QIdent _ | QLookupRef _ | QSource _ -> + (value_vars, constants, required)) + ([], [], []) + in + let duplicate_value_var = + let seen = Hashtbl.create (List.length value_var_patterns) in + List.exists + (fun (value_var, _) -> + if Hashtbl.mem seen value_var then true + else ( + Hashtbl.add seen value_var (); + false )) + value_var_patterns + in + if duplicate_value_var || required_patterns <> [] then + None + else + (match value_var_patterns, constant_patterns, excluded_patterns with + | [ (value_var, seed_attr) ], [], [ (clause_attr, clause_value) ] + when direct_attr seed_attr && direct_attr clause_attr -> + (match aevt_attr_array source_db seed_attr with + | None -> None + | Some seed_arr -> + let max_entity = source_db.max_datom_e + 1 in + let excluded = Bytes.make max_entity '\000' in + let mark_excluded entity_id = + if entity_id >= 0 && entity_id < max_entity then + Bytes.unsafe_set excluded entity_id '\001' + in + (match entity_ids_by_attr_value source_db clause_attr clause_value with + | Some entity_ids -> List.iter mark_excluded entity_ids + | None -> + datoms_by_attr_value source_db clause_attr clause_value + |> List.iter (fun datom -> mark_excluded datom.e)); + let rows = ref [] in + let emit datom = + if + datom.e >= 0 + && datom.e < max_entity + && Bytes.unsafe_get excluded datom.e = '\000' + then + match attrs with + | [ entity_attr; value_attr ] + when entity_attr = e_var && value_attr = value_var -> + rows := [ Result_entity datom.e; Query.result_of_datom_v datom ] :: !rows + | [ value_attr; entity_attr ] + when entity_attr = e_var && value_attr = value_var -> + rows := [ Query.result_of_datom_v datom; Result_entity datom.e ] :: !rows + | _ -> () + in + for i = Array.length seed_arr - 1 downto 0 do + emit seed_arr.(i) + done; + (match aevt_duplicate_datoms source_db seed_attr with + | [] -> () + | duplicates -> List.iter emit duplicates); + Some (attrs, !rows, unique_rows_flag attrs e_var)) + | value_vars, [ (const_attr, const_value) ], [] + when value_vars <> [] + && direct_attr const_attr + && List.for_all + (fun (_, attr) -> direct_attr attr && cardinality_one source_db attr) + value_vars -> + let value_vars = List.rev value_vars in + let avet_ids_array attr value = + if query_value_uses_avet value && query_attr_uses_avet source_db attr then + entity_ids_array_by_attr_value source_db attr value + else + None + in + let aligned_dense_rows () = + (match aevt_attr_array source_db const_attr with + | None -> None + | Some const_arr -> + let value_attr_arrays = + value_vars + |> List.map (fun (value_var, attr) -> + match aevt_attr_array source_db attr with + | None -> None + | Some arr -> Some (value_var, arr)) + in + if List.exists Option.is_none value_attr_arrays then + None + else + let value_attrs = value_attr_arrays |> List.map Option.get |> Array.of_list in + let attr_count = Array.length value_attrs in + let attr_arrays = Array.map (fun (_, arr) -> arr) value_attrs in + let const_len = Array.length const_arr in + if + const_len = 0 + || not (Array.for_all (fun arr -> Array.length arr = const_len) attr_arrays) + then + None + else + let mid = const_len / 2 in + let e_aligned = + let check i = + let e = const_arr.(i).e in + Array.for_all (fun arr -> arr.(i).e = e) attr_arrays + in + check 0 && check mid && check (const_len - 1) + in + if not e_aligned then + None + else + let base_e = const_arr.(0).e in + let dense = + const_arr.(const_len - 1).e = base_e + const_len - 1 + && Array.for_all + (fun arr -> + arr.(0).e = base_e && arr.(const_len - 1).e = base_e + const_len - 1) + attr_arrays + in + let specialized_find = + let expected = e_var :: (value_attrs |> Array.to_list |> List.map fst) in + attrs = expected + in + if not (specialized_find && dense) then + None + else + let rows = ref [] in + (match attr_count with + | 4 -> + let a0 = attr_arrays.(0) in + let a1 = attr_arrays.(1) in + let a2 = attr_arrays.(2) in + let a3 = attr_arrays.(3) in + (match avet_ids_array const_attr const_value with + | Some ids -> + for i = Array.length ids - 1 downto 0 do + let e = ids.(i) in + let index = e - base_e in + if index >= 0 && index < const_len then + rows := + [ Result_entity e + ; Result_value a0.(index).v + ; Result_value a1.(index).v + ; Result_value a2.(index).v + ; Result_value a3.(index).v + ] + :: !rows + done + | None -> + for i = const_len - 1 downto 0 do + if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 + then + rows := + [ Result_entity const_arr.(i).e + ; Result_value a0.(i).v + ; Result_value a1.(i).v + ; Result_value a2.(i).v + ; Result_value a3.(i).v + ] + :: !rows + done) + | 1 -> + let a0 = attr_arrays.(0) in + (match avet_ids_array const_attr const_value with + | Some ids -> + for i = Array.length ids - 1 downto 0 do + let e = ids.(i) in + let index = e - base_e in + if index >= 0 && index < const_len then + rows := + [ Result_entity e; Result_value a0.(index).v ] :: !rows + done + | None -> + for i = const_len - 1 downto 0 do + if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 + then + rows := + [ Result_entity const_arr.(i).e; Result_value a0.(i).v ] :: !rows + done) + | _ -> + (match avet_ids_array const_attr const_value with + | Some ids -> + for i = Array.length ids - 1 downto 0 do + let e = ids.(i) in + let index = e - base_e in + if index >= 0 && index < const_len then + let rec vals a acc = + if a < 0 then Result_entity e :: acc + else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) + in + rows := vals (attr_count - 1) [] :: !rows + done + | None -> + for i = const_len - 1 downto 0 do + if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 + then + let e = const_arr.(i).e in + let rec vals a acc = + if a < 0 then Result_entity e :: acc + else vals (a - 1) (Result_value attr_arrays.(a).(i).v :: acc) + in + rows := vals (attr_count - 1) [] :: !rows + done)); + if !rows = [] then None else Some !rows) + in + let intersect_value_dense_rows () = + let constant_entity_ids = + constant_patterns + |> List.map (fun (attr, value) -> + match entity_ids_by_attr_value source_db attr value with + | Some entity_ids -> entity_ids + | None -> + datoms_by_attr_value source_db attr value |> List.map (fun datom -> datom.e)) + in + if List.exists (fun ids -> ids = []) constant_entity_ids then + Some [] + else + let entity_ids = intersect_constant_entity_ids constant_entity_ids in + if entity_ids = [] then + Some [] + else + let value_attr_arrays = + value_vars + |> List.map (fun (value_var, attr) -> + match aevt_attr_array source_db attr with + | None -> None + | Some arr -> Some (value_var, arr)) + in + if List.exists Option.is_none value_attr_arrays then + None + else + let value_attrs = value_attr_arrays |> List.map Option.get |> Array.of_list in + let attr_count = Array.length value_attrs in + let attr_arrays = Array.map (fun (_, arr) -> arr) value_attrs in + let specialized_find = + let expected = e_var :: (value_attrs |> Array.to_list |> List.map fst) in + attrs = expected + in + if not specialized_find then + None + else + let first = attr_arrays.(0) in + let dense_len = Array.length first in + if dense_len = 0 then + None + else if not (Array.for_all (fun arr -> Array.length arr = dense_len) attr_arrays) + then + None + else + let base_e = first.(0).e in + if first.(dense_len - 1).e <> base_e + dense_len - 1 then + None + else + let mid = dense_len / 2 in + let aligned = + let check i = + let e = first.(i).e in + Array.for_all (fun arr -> arr.(i).e = e) attr_arrays + in + check 0 && check mid && check (dense_len - 1) + in + if not aligned then + None + else + let entities = + entity_ids |> ensure_sorted_entity_ids |> Array.of_list + in + let rows = ref [] in + for i = Array.length entities - 1 downto 0 do + let eid = entities.(i) in + let index = eid - base_e in + if index >= 0 && index < dense_len then + let rec vals a acc = + if a < 0 then Result_entity eid :: acc + else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) + in + rows := vals (attr_count - 1) [] :: !rows + done; + Some !rows + in + (match aligned_dense_rows () with + | Some rows -> Some (attrs, rows, unique_rows_flag attrs e_var) + | None -> ( + match intersect_value_dense_rows () with + | Some rows -> Some (attrs, rows, unique_rows_flag attrs e_var) + | None -> None)) + | _ -> None)) + | _ -> None + let eval_relation_rows db sources rules bindings clauses = let default_source = source db sources "$" in let try_single_pattern_rule_rows = @@ -3692,22 +4054,30 @@ end) = struct in match try_single_pattern_rule_rows with | Some result -> Some result - | None -> - match expand_inline_rules rules clauses with - | None -> None - | Some clauses -> - (match bindings, relation_query_clauses clauses with - | [ [] ], true -> ( - match same_entity_fused_relation db default_source clauses with - | Some relation -> Some (relation.attrs, relation.rows, relation.unique_rows) - | None -> - eval_relation_from_empty db sources default_source clauses - |> Option.map (fun relation -> relation.attrs, relation.rows, relation.unique_rows)) - | [ binding ], true -> - let clauses = List.map (bound_relation_clause binding) clauses in - eval_relation_from_empty db sources default_source clauses - |> Option.map (fun relation -> relation.attrs, relation.rows, relation.unique_rows) - | _ -> None) + | None -> ( + let continue () = + match if rules = [] then Some clauses else expand_inline_rules rules clauses with + | None -> None + | Some clauses -> + (match bindings, relation_query_clauses clauses with + | [ [] ], true -> ( + match same_entity_fused_relation db default_source clauses with + | Some relation -> Some (relation.attrs, relation.rows, relation.unique_rows) + | None -> + eval_relation_from_empty db sources default_source clauses + |> Option.map (fun relation -> relation.attrs, relation.rows, relation.unique_rows)) + | [ binding ], true -> + let clauses = List.map (bound_relation_clause binding) clauses in + eval_relation_from_empty db sources default_source clauses + |> Option.map (fun relation -> relation.attrs, relation.rows, relation.unique_rows) + | _ -> None) + in + match bindings, rules with + | [ [] ], [] -> ( + match try_fast_empty_relation_rows db default_source clauses with + | Some result -> Some result + | None -> continue ()) + | _ -> continue ()) let eval_relation_clauses ?(allow_initial_bindings = false) db sources default_source bindings clauses = let bound_relation_pattern_terms = function From 69794cc2e0b3b76319a12cf6edd3b8f2c00fb330 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 02:06:40 +0000 Subject: [PATCH 09/14] Add Datahike vs OCaml query pipeline comparison doc Clone replikativ/datahike under _deps/ for local reference. Document phase-by-phase mapping, per-bench-query behavior, and refactor plan to replace query_where special cases with query_exec fused execute. Co-authored-by: Tienson Qin --- docs/datahike-ocaml-query-comparison.md | 282 ++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 docs/datahike-ocaml-query-comparison.md diff --git a/docs/datahike-ocaml-query-comparison.md b/docs/datahike-ocaml-query-comparison.md new file mode 100644 index 0000000..167c692 --- /dev/null +++ b/docs/datahike-ocaml-query-comparison.md @@ -0,0 +1,282 @@ +# Datahike vs OCaml Query Implementation Comparison + +Reference clone: `_deps/datahike` (replikativ/datahike, shallow clone for local diff). +Upstream doc: `_deps/datahike/doc/query-engine.md`. + +Observable **results** must stay DataScript-compatible. **Execution architecture** +should follow Datahike's compiled planner + permanent relational fallback. + +## Pipeline mapping + +| Phase | Datahike | OCaml (this repo) | Gap | +| --- | --- | --- | --- | +| Entry | `datahike/query.cljc` → `q` / `execute-planned-direct` | `datascript.ml` → `Query_impl.q` → `query_api.ml` `q_sources_raw` | OK (no `simple_*` on `q`) | +| Classify | `query/analyze.cljc` `classify-clause` | Inline in `query_plan.ml` / `query_where.ml` pattern parsing | No dedicated analyze module | +| Logical IR | `query/logical.cljc` `build-logical-plan` | `query_plan.ml` `build_logical_plan` | Same node shapes (`LEntityJoin`, `LScan`, …) | +| Lower | `query/lower.cljc` + `query/plan.cljc` | `query_plan.ml` `lower` / `compile` | **Major**: DH uses DP merge + pipeline DSL; OCaml flattens to clause list | +| Execute | `query/execute.cljc` fused scan+merge, probe-map joins | **Missing** `query_exec.ml`; execution lives in `query_where.ml` | **Major**: no cursor merge, no `PPipeline` | +| Fallback | `query/relation.cljc` + `query.cljc` `execute-legacy` | `query_where.ml` relation interpreter | Permanent fallback — correct role, but also hosts fast paths | +| Project | find projection in execute / query | `query_api.ml` `relation_rows_for_find` | OK | + +Datahike end-to-end: + +``` +analyze → logical.cljc → lower.cljc → execute.cljc → find project + ↳ ineligible → relation.cljc (legacy) +``` + +OCaml today: + +``` +query_plan.compile → clauses_of_plan → query_where (fused kernels + interpreter) + ↳ try_fast_empty_relation_rows (pre-planner bypass) + ↳ relation_of_same_entity_patterns (dense AEVT gather) + ↳ eval_relation_from_empty (hash_join chain) +``` + +The planner IR **matches** Datahike; the **execute layer does not**. + +## Module-by-module notes + +### `analyze.cljc` (Datahike) + +- Classifies each clause: `:pattern`, `:predicate`, `:function`, `:not`, `:or`, … +- Extracts vars, checks fn args, handles quote forms. +- **OCaml**: scattered across `Query.pattern_scan`, `query_plan.pattern_scan`, `query_where` clause walks. No single classify API. + +### `logical.cljc` (Datahike) + +Key behaviors (see `build-logical-plan`): + +1. Classify all clauses → `LScan` / `LFilter` / `LBind` / … +2. Group scans by `[entity-var, source]` → `LEntityJoin` +3. **Foldable NOT** (`foldable-not?`): single-pattern NOT on grouped entity, non-entity vars local to negation → **anti-scan inside entity group** +4. Remaining NOT → `LAntiJoin` +5. OR / rules → `LUnion` / `LRuleCall` / `LFixpoint` + +**OCaml** (`query_plan.ml` `build_logical_plan`): + +- Same grouping and foldable-NOT idea (`foldable_not_scan`). +- Extra constraint: fold only if positive scan **earlier in source order** (DataScript outer-binding errors). +- Does **not** tag nodes with `:source-idx` for bound-var-card propagation (Datahike lower uses this). + +### `plan.cljc` + `lower.cljc` (Datahike) + +Physical planning primitives: + +| Primitive | Purpose | +| --- | --- | +| `plan-pattern-op` | Index choice (EAVT/AEVT/AVET) + pushdown bounds | +| `dp-order-fuse-ops` | Optimal scan + merge order within entity group | +| `assemble-entity-group` | `:entity-group` op + `build-pipeline` | +| `detect-inter-group-joins` | Shared value vars → hash-probe plan | +| `dp-order-groups` / `order-plan-ops` | Inter-group order + readiness | + +Lower produces ops like: + +```clojure +{:op :entity-group + :scan-op {... :index :aevt ...} + :merge-ops [{:join-method :lookup ...} ...] + :pipeline {:path :sorted-merge :steps [...]}} +``` + +**OCaml** (`query_plan.ml`): + +- `OpEntityGroup { clauses; estimated_rows }` — **only clause list**, no scan/merge split, no pipeline. +- `lower` schedules ops by heuristic cost; `clauses_of_plan` **discards physical structure**. +- Index choice exists (`choose_index`) but is not consumed by a fused executor. + +### `execute.cljc` (Datahike) + +Core execution paths: + +1. **`execute-group-direct`** — entity group fused scan: + - Pick driving scan (lowest cardinality after DP) + - Walk index slice; for each datom, **seekGE** merge lookups (no intermediate relations) + - Paths: `:scan-only`, `:sorted-merge`, `:per-cursor-merge`, `:card-many-merge` +2. **Anti-merge** — during merge loop, skip entities matching anti-scan attr/value +3. **Multi-group** — producer probe-set / probe-map → consumer filtered scan +4. **Post-filter / post-apply** — wide tuples then project to find-vars + +**OCaml** (`query_where.ml`): + +- `relation_of_same_entity_patterns` — materializes `{ attrs; rows }` lists +- Dense AEVT gather (`try_same_entity_constant_dense_rows`, `try_fast_empty_relation_rows`) — **ad hoc**, not driven by `OpEntityGroup` / pipeline +- `hash_join` on relations — correct fallback shape, not cursor merge +- NOT: bitset exclusion scan OR `anti_join` on relations + +### `relation.cljc` (Datahike fallback) + +- Tuple relations, `hash-join`, `sum-rel`, `subtract-rel` +- Used when planner ineligible or `*disable-planner*` + +**OCaml**: same concepts in `query_where.ml` (`hash_join`, `anti_join`, `union_relations`). + +## Shared bench queries — shape-by-shape + +Queries from `bench/shared_query_bench.ml`. + +### q1 — `[:find ?e :where [?e :name "Ivan"]]` + +| | Datahike | OCaml | +| --- | --- | --- | +| Logical | `LScan` (ground value → AVET) | `LScan` → `OpScan` or single-pattern group | +| Execute | AVET slice or EAVT seek; **no relation alloc** | AVET ids or AEVT scan → relation rows | +| Gap | Direct emit to result set | Extra `{attrs;rows}` wrapper | + +### q2 — `[:find ?e ?a :where [?e :name "Ivan"] [?e :age ?a]]` + +| | Datahike | OCaml | +| --- | --- | --- | +| Logical | `LEntityJoin` with 2 scans | Same | +| Lower | `assemble-entity-group`: DP picks scan (`:name` selective) + merge `:age` via **lookupGE** | `OpEntityGroup` → flat clauses → `try_fast_*` or `relation_of_same_entity_patterns` | +| Execute | **Fused sorted-merge** — one pass, no hash join | Dense AEVT index gather OR hash_join two relations | +| Perf | ~0.6 ms (20k entities, DH bench doc) | ~0.009 ms (2k entities) vs **0.004 ms** pre-removal gate | + +Root cause of OCaml gap: execution still **materializes row lists** and duplicates kernel logic outside the planner op stream. + +### q-5-merge — five attrs + `[?e :sex :male]` + +| | Datahike | OCaml | +| --- | --- | --- | +| Logical | `LEntityJoin` 5 scans + constant on `:sex` | Same | +| Execute | DP order: selective constant/attr as scan, merges via cursor | Const-first aligned AEVT gather (4 value vars) | +| DH doc | "5-clause entity merge" **2.4 ms** @ 20k | **0.046 ms** @ 2k vs **0.030 ms** baseline | + +Datahike uses **merge ordering + seekGE**, not "all arrays aligned then index by entity id". + +### q-not / q-not-join — `[?e :age ?a] (not [?e :sex :male])` + +| | Datahike | OCaml | +| --- | --- | --- | +| Logical | Foldable NOT → **anti-scan** inside `LEntityJoin` on `?e` | Same fold in `build_logical_plan` | +| Execute | Anti-merge during fused scan (skip excluded entities) | `try_not_single_value_aevt_scan` / bitset + full AEVT walk | +| Planner | NOT present → still plans positive leg | **`plan_ordered_clauses` skips compile when any NOT** — source order only | +| DH doc | NOT **3.8 ms** @ 20k | **0.025 ms** @ 2k vs **0.023 ms** baseline | + +OCaml NOT path never uses planner ordering; anti-scan is reimplemented in fallback, not as merge op. + +### q-or-join, q-rule + +| Query | Datahike | OCaml | +| --- | --- | --- | +| q-or-join | `LUnion` → branch execute → combine | `eval_or_branch_relations` / union | +| q-rule | `LRuleCall` → expand → plan body | `try_single_pattern_rule_rows` + inline rules | + +## What is wrong with current `query_where.ml` complexity + +These are **execute-layer** concerns implemented inside the **fallback module**: + +| Mechanism | Lines (approx) | Datahike equivalent | +| --- | --- | --- | +| `try_fast_empty_relation_rows` | ~350 | Should not exist — `execute.cljc` `execute-group-direct` | +| `try_same_entity_constant_dense_rows` | ~150 | `assemble-entity-group` + `execute-sorted-merge` | +| `rows_from_dense_aevt_gather` | ~200 | Pipeline `PIndexScan` → `PSortedMerge` → `PEmitTuple` | +| `same_entity_fused_relation` | wrapper | `OpEntityGroup` execution | +| `relation_of_same_entity_patterns` | ~1300 | Split: lower produces ops, execute consumes ops | + +Adding more special cases in `query_where` **diverges further** from Datahike. The alignment doc (`docs/datahike-query-alignment.md` P3–P4) already says physical ops should drive execution. + +## Recommended refactor (Datahike-faithful) + +### 1. Add `impl/query_exec.ml` (execute layer) + +```ocaml +val run : + db -> physical_plan -> query_source -> bindings -> + (string list * query_result list list * bool) option +``` + +Implement op dispatch matching Datahike: + +- `OpEntityGroup` → fused entity-group execute (port `execute-group-direct` / `execute-sorted-merge` using existing `aevt_attr_array`, `entity_ids_array_by_attr_value`, index seeks) +- `OpScan` → single pattern scan +- `OpUnion` → `union_relations` +- `OpAntiJoin` → `anti_join` +- `OpFilter` → filter relation or in-group attached pred +- `OpPassthrough` → return `None` (fallback) + +Move dense gather / bitset NOT / aligned multi-attr logic **into** entity-group execute keyed by pipeline path — delete `try_fast_*`. + +### 2. Extend physical IR (minimal) + +Extend `OpEntityGroup` to carry what lower already knows: + +```ocaml +| OpEntityGroup of { + entity_var : string; + scan : l_scan; (* driving pattern *) + merges : l_scan list; (* DP-ordered *) + anti_scans : l_scan list; + filters : query_clause list; + index : index_choice; + ... + } +``` + +Stop flattening to `clauses` in `clauses_of_plan` for execution (keep flatten for tests/explain only). + +### 3. Wire entry (`query_api.ml`) + +```ocaml +match Query_plan.compile db.max_datom_e [] [] where with +| Some plan when Query_plan.plan_is_executable plan -> + (match Query_exec.run db plan default_source bindings with + | Some result -> ... + | None -> fallback interpreter) +| None -> fallback interpreter +``` + +Remove `try_fast_empty_relation_rows` bypass from `eval_relation_rows`. + +### 4. Keep `query_where.ml` as fallback only + +- `eval_relation_from_empty` / binding interpreter +- `hash_join`, `anti_join`, `union_relations`, `relation_of_pattern` +- Source-order NOT for DataScript error parity +- **No** bench-shaped dense kernels at module top level + +### 5. Port planning primitives incrementally + +Priority for bench perf: + +1. `dp-order-fuse-ops` (scan + merge order within group) — `plan.cljc:531` +2. `assemble-entity-group` + `build-pipeline` — `plan.cljc:831`, `:617` +3. `execute-sorted-merge` — `execute.cljc:1375` (card-one attrs, dense DBs) +4. Anti-merge in merge loop — NOT as separate full-DB bitset scan +5. Count-slice estimates — `estimate.cljc` (replace `max_e/8` heuristics) + +## File reference index (Datahike) + +| File | LOC (approx) | Read first | +| --- | --- | --- | +| `doc/query-engine.md` | 523 | Architecture overview | +| `src/datahike/query/ir.cljc` | 172 | IR + pipeline record defs | +| `src/datahike/query/analyze.cljc` | large | Clause classification | +| `src/datahike/query/logical.cljc` | 453 | `build-logical-plan`, NOT fold | +| `src/datahike/query/plan.cljc` | 1860 | DP merge, entity group, ordering | +| `src/datahike/query/lower.cljc` | medium | Logical → physical | +| `src/datahike/query/execute.cljc` | 6500+ | Fused scan, probe joins | +| `src/datahike/query/relation.cljc` | 300 | Fallback relations | +| `src/datahike/query.cljc` | 5300+ | Entry, planner eligibility | + +## Immediate action items + +1. **Stop expanding** `try_fast_empty_relation_rows` / `relation_of_same_entity_patterns` special cases. +2. **Implement** `query_exec.ml` with `OpEntityGroup` fused path for q1/q2/q-5-merge shapes. +3. **Extend** `query_plan.ml` `OpEntityGroup` to retain scan/merge structure (mirror `assemble-entity-group`). +4. **Delete** redundant dense kernels once execute path covers bench suite. +5. **Verify**: `test_shared_queries` + `shared_query_bench --size 2000` vs `3547876` baselines. + +## Local clone usage + +```bash +# Already cloned (gitignored) +ls _deps/datahike/src/datahike/query/ + +# Diff logical IR grouping +diff -u \ + <(rg -n 'LEntityJoin|foldable-not' _deps/datahike/src/datahike/query/logical.cljc) \ + <(rg -n 'LEntityJoin|foldable_not' impl/query_plan.ml) +``` From ea123625ebf54b91a08a5b5b1fd0f1e59a874fac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 02:15:59 +0000 Subject: [PATCH 10/14] Add query_exec layer and simplify query_where fallback - Extend OpEntityGroup IR with scan/merges/anti_scans/filters (Datahike shape) - Add impl/query_exec.ml: fused entity-group execute for single OpEntityGroup/OpScan - Wire query_api: compile -> Query_exec.run -> relational fallback - Remove try_fast_empty_relation_rows and ~1500 lines of duplicate dense kernels - Simplify relation_of_same_entity_patterns to hash_join fallback only - Route only plan_is_fused_execute shapes through execute; OR/NOT-join keep fallback Parity: test_shared_queries 17/17 green. Bench @ size=2000: q2 0.011, q-5-merge 0.055, q-or-join 0.051, q-rule 0.0095. Co-authored-by: Tienson Qin --- impl/datascript.ml | 24 +- impl/datascript.mli | 18 +- impl/query_api.ml | 25 +- impl/query_exec.ml | 525 +++++++++++++++ impl/query_exec.mli | 37 ++ impl/query_plan.ml | 32 +- impl/query_plan.mli | 23 +- impl/query_where.ml | 1547 +------------------------------------------ 8 files changed, 697 insertions(+), 1534 deletions(-) create mode 100644 impl/query_exec.ml create mode 100644 impl/query_exec.mli diff --git a/impl/datascript.ml b/impl/datascript.ml index 3344fc0..76dc5e1 100644 --- a/impl/datascript.ml +++ b/impl/datascript.ml @@ -1494,9 +1494,7 @@ module Query_where_impl = Query_where.Make (struct let normalize_value = normalize_value let datoms_by_attr_value = datoms_by_attr_value let entity_ids_by_attr_value = entity_ids_by_attr_value - let entity_ids_array_by_attr_value = entity_ids_array_by_attr_value let query_attr_uses_avet = query_attr_uses_avet - let query_value_uses_avet = query_value_uses_avet let fold_index_range = fold_index_range let find_entity_attr_value db entity_id attr = match Db.find_primary_aevt_entity_attr db entity_id attr with @@ -1508,6 +1506,27 @@ module Query_where_impl = Query_where.Make (struct let find_entity_in_aevt_array = Db.find_entity_in_aevt_array end) +module Query_exec = Query_exec + +module Query_exec_impl = Query_exec.Make (struct + let query_evaluator_context = query_evaluator_context + let query_source_context = query_source_context + let cardinality_one db attr = cardinality db attr = One + let datoms_by_attr_value = datoms_by_attr_value + let entity_ids_by_attr_value = entity_ids_by_attr_value + let entity_ids_array_by_attr_value = entity_ids_array_by_attr_value + let query_attr_uses_avet = query_attr_uses_avet + let query_value_uses_avet = query_value_uses_avet + let aevt_attr_array = Db.aevt_attr_array + let aevt_duplicate_datoms db attr = + Option.value (Hashtbl.find_opt db.duplicate_aevt_by_attr attr) ~default:[] +end) + +let execute_plan db sources rules bindings plan = + match Query_exec_impl.run db sources rules bindings plan with + | None -> None + | Some relation -> Some (relation.attrs, relation.rows, relation.unique_rows) + let eval_clauses = Query_where_impl.eval_clauses let eval_relation_rows = Query_where_impl.eval_relation_rows @@ -1673,6 +1692,7 @@ module Query_api_impl = Query_api.Make (struct let initial_query_context = initial_query_context let eval_clauses = eval_clauses let eval_relation_rows = eval_relation_rows + let execute_plan = execute_plan let has_aggregates = has_aggregates let aggregate_rows = aggregate_rows let aggregate_rows_with = aggregate_rows_with diff --git a/impl/datascript.mli b/impl/datascript.mli index 2877d74..311ac8d 100644 --- a/impl/datascript.mli +++ b/impl/datascript.mli @@ -457,13 +457,19 @@ module Query_plan : sig ; bound_vars : string list } + type entity_group = + { entity_var : string + ; scan : l_scan + ; merges : l_scan list + ; anti_scans : l_scan list + ; filters : query_clause list + ; clauses : query_clause list + ; estimated_rows : int + ; source : string option + } + type physical_op = - | OpEntityGroup of - { entity_var : string - ; clauses : query_clause list - ; estimated_rows : int - ; source : string option - } + | OpEntityGroup of entity_group | OpScan of { clause : query_clause ; index : index_choice diff --git a/impl/query_api.ml b/impl/query_api.ml index 5f2b4e4..f68c91f 100644 --- a/impl/query_api.ml +++ b/impl/query_api.ml @@ -24,6 +24,13 @@ module Make (Context : sig bindings list -> query_clause list -> (string list * query_result list list * bool) option + val execute_plan : + db -> + (string * query_source) list -> + query_rule list -> + bindings list -> + Query_plan.physical_plan -> + (string list * query_result list list * bool) option val has_aggregates : find_spec list -> bool val aggregate_rows : ?callables:Query.query_callables -> db -> (string * query_source) list -> bindings list -> find_spec list -> query_result list list val aggregate_rows_with : ?callables:Query.query_callables -> db -> (string * query_source) list -> bindings list -> find_spec list -> string list -> query_result list list @@ -150,8 +157,22 @@ end) = struct |> List.map snd let q_sources_raw ?(inputs = []) db sources query = - let finish_relation_rows rules input_bindings where find = - match eval_relation_rows db sources rules input_bindings where with + let finish_relation_rows rules input_bindings where find = + let try_planned_execute () = + if input_bindings = [ [] ] && rules = [] then + match Query_plan.compile ~max_datom_e:db.max_datom_e where with + | Some plan when Query_plan.plan_is_fused_execute plan -> + execute_plan db sources rules input_bindings plan + | _ -> None + else + None + in + let relation_result = + match try_planned_execute () with + | Some result -> Some result + | None -> eval_relation_rows db sources rules input_bindings where + in + match relation_result with | Some (attrs, rows, unique_rows) -> (match relation_rows_for_find db sources attrs rows unique_rows find with | Some rows -> rows diff --git a/impl/query_exec.ml b/impl/query_exec.ml new file mode 100644 index 0000000..da48cea --- /dev/null +++ b/impl/query_exec.ml @@ -0,0 +1,525 @@ +(** Datahike-aligned query execute layer: run compiled physical ops. *) + +open Datascript_types + +[@@@ocaml.warning "-67"] + +type bindings = (string * query_result) list + +type relation = + { attrs : string list + ; rows : query_result list list + ; unique_rows : bool + } + +module Make (Context : sig + val query_evaluator_context : Query_eval.evaluator_context + val query_source_context : db -> Query.source_context + val cardinality_one : db -> attr -> bool + val datoms_by_attr_value : db -> attr -> value -> datom list + val entity_ids_by_attr_value : db -> attr -> value -> entity_id list option + val entity_ids_array_by_attr_value : db -> attr -> value -> entity_id array option + val query_attr_uses_avet : db -> attr -> bool + val query_value_uses_avet : value -> bool + val aevt_attr_array : db -> attr -> datom array option + val aevt_duplicate_datoms : db -> attr -> datom list +end) = struct + open Context + + let ( let* ) = Option.bind + + let unique_vars terms = + terms + |> List.filter_map (function QVar name -> Some name | _ -> None) + |> List.fold_left (fun vars var -> if List.mem var vars then vars else var :: vars) [] + |> List.rev + + let row_value row index = + let rec loop current = function + | [] -> invalid_arg "relation row is missing a value" + | value :: _ when current = index -> value + | _ :: rest -> loop (current + 1) rest + in + loop 0 row + + let relation_attr_index attrs attr = + match List.find_index (( = ) attr) attrs with + | Some index -> index + | None -> invalid_arg "relation attribute is missing from row" + + let hash_join left right = + let common = List.filter (fun attr -> List.mem attr right.attrs) left.attrs in + let right_only = List.filter (fun attr -> not (List.mem attr left.attrs)) right.attrs in + let attrs = left.attrs @ right_only in + if left.attrs = [] && left.rows = [ [] ] then + { right with attrs } + else if right.attrs = [] && right.rows = [ [] ] then + { left with attrs } + else if common = [] then + { attrs + ; rows = + List.concat_map + (fun left_row -> List.map (fun right_row -> left_row @ right_row) right.rows) + left.rows + ; unique_rows = false + } + else + let right_common_indexes = List.map (fun attr -> attr, relation_attr_index right.attrs attr) common in + let right_by_key = + right.rows + |> List.fold_left + (fun table row -> + let key = + right_common_indexes + |> List.map (fun (attr, index) -> attr, row_value row index) + in + Hashtbl.replace table key row; + table) + (Hashtbl.create (List.length right.rows)) + in + let left_common_indexes = List.map (fun attr -> attr, relation_attr_index left.attrs attr) common in + let right_only_indexes = List.map (relation_attr_index right.attrs) right_only in + let rows = + left.rows + |> List.concat_map (fun left_row -> + let key = + left_common_indexes |> List.map (fun (attr, index) -> attr, row_value left_row index) + in + match Hashtbl.find_opt right_by_key key with + | None -> [] + | Some right_row -> + let extra = List.map (fun index -> row_value right_row index) right_only_indexes in + [ left_row @ extra ]) + in + { attrs; rows; unique_rows = left.unique_rows && right.unique_rows && rows <> [] } + + let anti_join left right = + let join_attrs = List.filter (fun attr -> List.mem attr right.attrs) left.attrs in + if join_attrs = [] then + Some left + else + let indexes = List.map (fun attr -> attr, relation_attr_index left.attrs attr) join_attrs in + let excluded = + right.rows + |> List.fold_left + (fun table row -> + let key = indexes |> List.map (fun (attr, index) -> attr, row_value row index) in + Hashtbl.replace table key (); + table) + (Hashtbl.create (List.length right.rows)) + in + let rows = + left.rows + |> List.filter (fun row -> + let key = indexes |> List.map (fun (attr, index) -> attr, row_value row index) in + not (Hashtbl.mem excluded key)) + in + Some { left with rows; unique_rows = left.unique_rows && rows <> [] } + + let eval_comparison_predicate_clause = Query_eval.eval_comparison_predicate_clause query_evaluator_context + + let filter_comparison db relation predicate left_term right_term = + let rows = + relation.rows + |> List.filter (fun row -> + let binding = List.combine relation.attrs row in + eval_comparison_predicate_clause db binding predicate left_term right_term <> []) + in + { relation with rows; unique_rows = false } + + let empty_relation = { attrs = []; rows = [ [] ]; unique_rows = true } + + let direct_attr attr = not (query_evaluator_context.is_reverse_ref attr) + + let unique_rows_flag source_db attrs e_var = + (not source_db.history) + && source_db.duplicate_datoms = [] + && List.mem e_var attrs + + let classify_patterns e_var scans = + scans + |> List.fold_left + (fun (value_vars, constants, required) (_, attr, value_term) -> + match value_term with + | QVar value_var when value_var <> e_var -> + ((value_var, attr) :: value_vars, constants, required) + | QValue value -> (value_vars, (attr, value) :: constants, required) + | QWildcard -> (value_vars, constants, attr :: required) + | QVar _ | QEntity _ | QAttr _ | QIdent _ | QLookupRef _ | QSource _ -> + (value_vars, constants, required)) + ([], [], []) + + let attrs_of_scans e_var scans = + scans + |> List.concat_map (fun (_, attr, value_term) -> [ QVar e_var; QAttr attr; value_term ]) + |> unique_vars + + let attr_name = function QAttr name -> name | _ -> "" + + let scans_of_group (group : Query_plan.entity_group) = + List.map + (fun (scan : Query_plan.l_scan) -> + match scan.entity with + | QVar e_var -> e_var, attr_name scan.attr, scan.value + | _ -> "", attr_name scan.attr, scan.value) + (group.scan :: group.merges) + + let anti_patterns_of_group (group : Query_plan.entity_group) = + List.map + (fun (anti : Query_plan.l_scan) -> attr_name anti.attr, anti.value) + group.anti_scans + + let avet_ids_array source_db attr value = + if query_value_uses_avet value && query_attr_uses_avet source_db attr then + entity_ids_array_by_attr_value source_db attr value + else + None + + let arrays_aligned const_arr attr_arrays = + let const_len = Array.length const_arr in + if const_len = 0 then + false + else if not (Array.for_all (fun arr -> Array.length arr = const_len) attr_arrays) then + false + else + let mid = const_len / 2 in + let check i = + let e = const_arr.(i).e in + Array.for_all (fun arr -> arr.(i).e = e) attr_arrays + in + check 0 && check mid && check (const_len - 1) + + let dense_range const_arr attr_arrays = + let const_len = Array.length const_arr in + let base_e = const_arr.(0).e in + const_arr.(const_len - 1).e = base_e + const_len - 1 + && Array.for_all (fun arr -> arr.(0).e = base_e && arr.(const_len - 1).e = base_e + const_len - 1) attr_arrays + + let build_value_row e attr_arrays index = + let rec vals a acc = + if a < 0 then Result_entity e :: acc + else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) + in + vals (Array.length attr_arrays - 1) [] + + let gather_const_value_rows source_db e_var attrs const_attr const_value value_vars = + let value_vars = List.rev value_vars in + if + value_vars = [] + || not (direct_attr const_attr) + || not (List.for_all (fun (_, attr) -> direct_attr attr && cardinality_one source_db attr) value_vars) + then + None + else + let* const_arr = aevt_attr_array source_db const_attr in + let value_attr_arrays = + value_vars + |> List.map (fun (value_var, attr) -> + match aevt_attr_array source_db attr with + | None -> None + | Some arr -> Some (value_var, arr)) + in + if List.exists Option.is_none value_attr_arrays then + None + else + let value_attrs = value_attr_arrays |> List.map Option.get |> Array.of_list in + let attr_arrays = Array.map (fun (_, arr) -> arr) value_attrs in + if not (arrays_aligned const_arr attr_arrays) then + None + else + let const_len = Array.length const_arr in + let base_e = const_arr.(0).e in + if not (dense_range const_arr attr_arrays) then + None + else + let expected = e_var :: (value_attrs |> Array.to_list |> List.map fst) in + if attrs <> expected then + None + else + let rows = ref [] in + (match avet_ids_array source_db const_attr const_value with + | Some ids -> + for i = Array.length ids - 1 downto 0 do + let e = ids.(i) in + let index = e - base_e in + if index >= 0 && index < const_len then + rows := build_value_row e attr_arrays index :: !rows + done + | None -> + for i = const_len - 1 downto 0 do + if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 then + rows := build_value_row const_arr.(i).e attr_arrays i :: !rows + done); + Some !rows + + let intersect_entity_ids id_lists = + let rec intersect_sorted left right = + match left, right with + | [], _ | _, [] -> [] + | x :: xs, y :: ys -> + if x = y then x :: intersect_sorted xs ys + else if x < y then intersect_sorted xs right + else intersect_sorted left ys + in + match List.sort (fun left right -> compare (List.length left) (List.length right)) id_lists with + | [] -> [] + | smallest :: rest -> List.fold_left intersect_sorted smallest rest + + let entity_ids_for_constant source_db attr value = + match entity_ids_by_attr_value source_db attr value with + | Some entity_ids -> entity_ids + | None -> datoms_by_attr_value source_db attr value |> List.map (fun datom -> datom.e) + + let gather_multi_constant_value_rows source_db e_var attrs constants value_vars = + if + constants = [] + || value_vars = [] + || not + (List.for_all + (fun (_, attr) -> direct_attr attr && cardinality_one source_db attr) + value_vars) + then + None + else + let entity_sets = List.map (fun (attr, value) -> entity_ids_for_constant source_db attr value) constants in + if List.exists (fun ids -> ids = []) entity_sets then + Some [] + else + let allowed = intersect_entity_ids entity_sets in + if allowed = [] then + Some [] + else + match constants, value_vars with + | [ (const_attr, const_value) ], _ -> + gather_const_value_rows source_db e_var attrs const_attr const_value value_vars + | _ :: _, value_vars -> ( + let allowed_set = + let bytes = Bytes.make (source_db.max_datom_e + 1) '\000' in + List.iter (fun e -> if e >= 0 && e < Bytes.length bytes then Bytes.set bytes e '\001') allowed; + bytes + in + let filter_rows rows = + List.filter + (fun row -> + match row with + | Result_entity e :: _ -> e >= 0 && e < Bytes.length allowed_set && Bytes.get allowed_set e = '\001' + | _ -> false) + rows + in + let (const_attr, const_value) = List.hd constants in + match gather_const_value_rows source_db e_var attrs const_attr const_value value_vars with + | None -> None + | Some rows -> Some (filter_rows rows)) + | _ -> None + + let gather_not_rows source_db e_var attrs seed_attr value_var clause_attr clause_value = + if not (direct_attr seed_attr && direct_attr clause_attr) then + None + else + let* seed_arr = aevt_attr_array source_db seed_attr in + let max_entity = source_db.max_datom_e + 1 in + let excluded = Bytes.make max_entity '\000' in + let mark_excluded entity_id = + if entity_id >= 0 && entity_id < max_entity then Bytes.unsafe_set excluded entity_id '\001' + in + (match entity_ids_by_attr_value source_db clause_attr clause_value with + | Some entity_ids -> List.iter mark_excluded entity_ids + | None -> datoms_by_attr_value source_db clause_attr clause_value |> List.iter (fun datom -> mark_excluded datom.e)); + let rows = ref [] in + let emit datom = + if datom.e >= 0 && datom.e < max_entity && Bytes.unsafe_get excluded datom.e = '\000' then + match attrs with + | [ entity_attr; value_attr ] when entity_attr = e_var && value_attr = value_var -> + rows := [ Result_entity datom.e; Query.result_of_datom_v datom ] :: !rows + | [ value_attr; entity_attr ] when entity_attr = e_var && value_attr = value_var -> + rows := [ Query.result_of_datom_v datom; Result_entity datom.e ] :: !rows + | _ -> () + in + for i = Array.length seed_arr - 1 downto 0 do + emit seed_arr.(i) + done; + List.iter emit (aevt_duplicate_datoms source_db seed_attr); + Some !rows + + let execute_entity_group db source (group : Query_plan.entity_group) = + match source with + | Db_source source_db -> + let scans = scans_of_group group in + let e_var = group.entity_var in + if not (List.for_all (fun (candidate, _, _) -> candidate = e_var) scans) then + None + else + let attrs = attrs_of_scans e_var scans + in + let value_var_patterns, constant_patterns, required_patterns = + classify_patterns e_var scans + in + let duplicate_value_var = + let seen = Hashtbl.create (List.length value_var_patterns) in + List.exists + (fun (value_var, _) -> + if Hashtbl.mem seen value_var then true + else ( + Hashtbl.add seen value_var (); + false )) + value_var_patterns + in + if duplicate_value_var || required_patterns <> [] then + None + else + let anti = anti_patterns_of_group group in + (match value_var_patterns, constant_patterns, anti with + | [ (value_var, seed_attr) ], [], [ (clause_attr, QValue clause_value) ] -> + gather_not_rows source_db e_var attrs seed_attr value_var clause_attr clause_value + | value_vars, constants, [] when value_vars <> [] && constants <> [] -> ( + match constants with + | [ (const_attr, const_value) ] -> + gather_const_value_rows source_db e_var attrs const_attr const_value value_vars + | _ -> + gather_multi_constant_value_rows source_db e_var attrs constants value_vars) + | [], [ (const_attr, const_value) ], [] -> ( + match entity_ids_by_attr_value source_db const_attr const_value with + | Some entity_ids -> Some (List.map (fun e -> [ Result_entity e ]) entity_ids) + | None -> + Some + (datoms_by_attr_value source_db const_attr const_value + |> List.map (fun datom -> [ Result_entity datom.e ]))) + | _ -> None) + |> Option.map (fun rows -> + let relation = { attrs; rows; unique_rows = unique_rows_flag source_db attrs e_var } in + List.fold_left + (fun relation clause -> + match clause with + | ComparisonPredicate (predicate, left_term, right_term) -> + filter_comparison db relation predicate left_term right_term + | _ -> relation) + relation + group.filters) + | _ -> None + + let execute_scan db source (scan : Query_plan.l_scan) = + match source with + | Db_source source_db -> + let terms = + match scan.tx with + | None -> [ scan.entity; scan.attr; scan.value ] + | Some tx -> [ scan.entity; scan.attr; scan.value; tx ] + in + let attrs = unique_vars terms in + let source_context = query_source_context db in + let datoms = + match terms with + | [ e_term; a_term; v_term ] -> source_context.pattern_datoms source_db e_term a_term v_term None + | [ e_term; a_term; v_term; tx_term ] -> source_context.pattern_datoms source_db e_term a_term v_term (Some tx_term) + | _ -> invalid_arg "scan expects 3 or 4 pattern terms" + in + let slots = + attrs + |> List.map (fun attr -> + let rec find index = function + | [] -> invalid_arg "scan variable missing from pattern" + | QVar var :: _ when var = attr -> index + | _ :: rest -> find (index + 1) rest + in + find 0 terms) + in + let build_row datom = + slots + |> List.map (fun index -> + match index with + | 0 -> Query.result_of_datom_e datom + | 1 -> Query.result_of_datom_a datom + | 2 -> Query.result_of_ref (Query.result_of_datom_v datom) + | 3 -> Query.result_of_datom_tx datom + | _ -> invalid_arg "invalid scan slot") + in + let rows = + datoms + |> Seq.fold_left (fun acc datom -> build_row datom :: acc) [] + |> List.rev + in + Some { attrs; rows; unique_rows = false } + | _ -> None + + let rec execute_plan db sources default_source bindings plan = + let rec apply relation = function + | [] -> Some relation + | Query_plan.OpEntityGroup group :: rest -> ( + match execute_entity_group db default_source group with + | None -> None + | Some next -> apply (hash_join relation next) rest) + | Query_plan.OpScan { clause; source = op_source; _ } :: rest -> ( + let source = + match op_source with + | Some name -> Query.source db sources name + | None -> default_source + in + match Query_plan.pattern_scan clause with + | None -> None + | Some scan -> ( + match execute_scan db source scan with + | None -> None + | Some next -> apply (hash_join relation next) rest)) + | Query_plan.OpFilter clause :: rest -> ( + match clause with + | ComparisonPredicate (predicate, left_term, right_term) -> + apply (filter_comparison db relation predicate left_term right_term) rest + | _ -> None) + | Query_plan.OpUnion { join_vars; branches } :: rest -> ( + let branch_relations = + branches + |> List.filter_map (fun branch -> execute_plan db sources default_source bindings branch) + in + if List.length branch_relations <> List.length branches then + None + else + let* merged = + match branch_relations with + | [] -> Some empty_relation + | first :: others -> + Some + (List.fold_left + (fun acc branch -> + match join_vars with + | None -> union_relations acc branch + | Some vars -> union_relations (project_relation vars acc) (project_relation vars branch)) + first + others) + in + apply (hash_join relation merged) rest) + | Query_plan.OpAntiJoin { join_vars; excluded } :: rest -> ( + let* excluded_relation = execute_plan db sources default_source bindings excluded in + let filtered = + match join_vars with + | None -> relation + | Some vars -> project_relation vars relation + in + let* joined = anti_join filtered excluded_relation in + apply joined rest) + | Query_plan.OpPassthrough _ :: _ -> None + in + apply empty_relation plan.ops + + and union_relations left right = + let attrs = left.attrs @ List.filter (fun attr -> not (List.mem attr left.attrs)) right.attrs in + let rows = left.rows @ right.rows |> List.sort_uniq compare in + { attrs; rows; unique_rows = false } + + and project_relation vars relation = + let indexes = vars |> List.map (relation_attr_index relation.attrs) in + let attrs = vars in + let rows = + relation.rows + |> List.filter_map (fun row -> + try Some (indexes |> List.map (fun index -> row_value row index)) with _ -> None) + |> List.sort_uniq compare + in + { attrs; rows; unique_rows = false } + + let run db sources rules bindings plan = + if rules <> [] || bindings <> [ [] ] then + None + else + let default_source = Query.source db sources "$" in + execute_plan db sources default_source bindings plan +end diff --git a/impl/query_exec.mli b/impl/query_exec.mli new file mode 100644 index 0000000..ff5e5ad --- /dev/null +++ b/impl/query_exec.mli @@ -0,0 +1,37 @@ +(** Datahike-aligned query execute layer: run compiled physical ops. + + Returns [None] when a shape is not executable here; callers use the + relational interpreter in [Query_where] as permanent fallback. *) + +open Datascript_types + +[@@@ocaml.warning "-67"] + +type bindings = (string * query_result) list + +type relation = + { attrs : string list + ; rows : query_result list list + ; unique_rows : bool + } + +module Make (Context : sig + val query_evaluator_context : Query_eval.evaluator_context + val query_source_context : db -> Query.source_context + val cardinality_one : db -> attr -> bool + val datoms_by_attr_value : db -> attr -> value -> datom list + val entity_ids_by_attr_value : db -> attr -> value -> entity_id list option + val entity_ids_array_by_attr_value : db -> attr -> value -> entity_id array option + val query_attr_uses_avet : db -> attr -> bool + val query_value_uses_avet : value -> bool + val aevt_attr_array : db -> attr -> datom array option + val aevt_duplicate_datoms : db -> attr -> datom list +end) : sig + val run : + db -> + (string * query_source) list -> + query_rule list -> + bindings list -> + Query_plan.physical_plan -> + relation option +end diff --git a/impl/query_plan.ml b/impl/query_plan.ml index b71ccb7..08a5dc4 100644 --- a/impl/query_plan.ml +++ b/impl/query_plan.ml @@ -52,13 +52,19 @@ and logical_plan = ; bound_vars : string list } +type entity_group = + { entity_var : string + ; scan : l_scan + ; merges : l_scan list + ; anti_scans : l_scan list + ; filters : query_clause list + ; clauses : query_clause list + ; estimated_rows : int + ; source : string option + } + type physical_op = - | OpEntityGroup of - { entity_var : string - ; clauses : query_clause list - ; estimated_rows : int - ; source : string option - } + | OpEntityGroup of entity_group | OpScan of { clause : query_clause ; index : index_choice @@ -436,10 +442,19 @@ let rec lower_node ~max_datom_e = function if cmp <> 0 then cmp else compare i1 i2) |> List.map (fun (_, _, s) -> s) in + let scan, merges = + match ordered_scans with + | [] -> invalid_arg "entity join requires at least one scan" + | driving :: rest -> driving, rest + in let pattern_clauses = List.map (fun s -> s.clause) ordered_scans in let anti_clauses = List.map (fun s -> Not [ s.clause ]) anti_scans in OpEntityGroup { entity_var + ; scan + ; merges + ; anti_scans + ; filters ; clauses = pattern_clauses @ anti_clauses @ filters ; estimated_rows = entity_group_cost ~max_datom_e ordered_scans ; source @@ -613,6 +628,11 @@ let analyze ?(max_datom_e = 1_000_000) ?(bound_vars = []) ?(rules = []) query = let plan_is_executable plan = not (List.exists (function OpPassthrough _ -> true | _ -> false) plan.ops) +let plan_is_fused_execute plan = + match plan.ops with + | [ OpEntityGroup _ ] | [ OpScan _ ] -> true + | _ -> false + let rec clauses_of_plan plan = plan.ops |> List.concat_map (function diff --git a/impl/query_plan.mli b/impl/query_plan.mli index 3d33d9e..30b5ceb 100644 --- a/impl/query_plan.mli +++ b/impl/query_plan.mli @@ -52,13 +52,19 @@ and logical_plan = ; bound_vars : string list } +type entity_group = + { entity_var : string + ; scan : l_scan + ; merges : l_scan list + ; anti_scans : l_scan list + ; filters : query_clause list + ; clauses : query_clause list + ; estimated_rows : int + ; source : string option + } + type physical_op = - | OpEntityGroup of - { entity_var : string - ; clauses : query_clause list - ; estimated_rows : int - ; source : string option - } + | OpEntityGroup of entity_group | OpScan of { clause : query_clause ; index : index_choice @@ -80,6 +86,8 @@ and physical_plan = { ops : physical_op list } +val pattern_scan : query_clause -> l_scan option + (** Ground-component index preference (Datahike plan-pattern-op). *) val choose_index : query_term -> query_term -> query_term -> index_choice @@ -103,5 +111,8 @@ val analyze : ?max_datom_e:int -> ?bound_vars:string list -> ?rules:query_rule l (** True when every op is planner-executable (no [OpPassthrough]). *) val plan_is_executable : physical_plan -> bool +(** True when the plan is a single fused entity-group or scan for [Query_exec]. *) +val plan_is_fused_execute : physical_plan -> bool + (** Flatten a physical plan back to where-clauses in execution order (tests / explain). *) val clauses_of_plan : physical_plan -> query_clause list diff --git a/impl/query_where.ml b/impl/query_where.ml index fd6f177..4452633 100644 --- a/impl/query_where.ml +++ b/impl/query_where.ml @@ -27,9 +27,7 @@ module Make (Context : sig val normalize_value : value -> value val datoms_by_attr_value : db -> attr -> value -> datom list val entity_ids_by_attr_value : db -> attr -> value -> entity_id list option - val entity_ids_array_by_attr_value : db -> attr -> value -> entity_id array option val query_attr_uses_avet : db -> attr -> bool - val query_value_uses_avet : value -> bool val fold_index_range : ('acc -> datom -> 'acc) -> 'acc -> db -> attr -> ?start:value -> ?stop:value -> unit -> 'acc val find_entity_attr_value : db -> entity_id -> attr -> query_result option @@ -1305,1149 +1303,41 @@ end) = struct |> unique_vars in let lookup_vars = relation_lookup_vars source_db [ QVar e_var; QWildcard; QWildcard ] in - let avet_ids_array attr value = - if - (not (query_evaluator_context.is_reverse_ref attr)) - && query_value_uses_avet value - && query_attr_uses_avet source_db attr - then - entity_ids_array_by_attr_value source_db attr value - else - None - in - let try_same_entity_constant_dense_rows = - if has_not then - None - else - match constant_patterns, value_var_patterns, required_patterns, excluded_patterns, relation_comparisons with - | [ (const_attr, const_value) ], value_vars, [], [], [] - when value_vars <> [] - && not (query_evaluator_context.is_reverse_ref const_attr) - && List.for_all - (fun (_, attr) -> - not (query_evaluator_context.is_reverse_ref attr) - && cardinality_one source_db attr) - value_vars -> - let value_vars = List.rev value_vars in - (match aevt_attr_array source_db const_attr with - | None -> None - | Some const_arr -> - let value_attr_arrays = - value_vars - |> List.map (fun (value_var, attr) -> - match aevt_attr_array source_db attr with - | None -> None - | Some arr -> Some (value_var, arr)) - in - if List.exists Option.is_none value_attr_arrays then - None - else - let value_attrs = value_attr_arrays |> List.map Option.get |> Array.of_list in - let attr_count = Array.length value_attrs in - let attr_arrays = Array.map (fun (_, arr) -> arr) value_attrs in - let const_len = Array.length const_arr in - if - const_len = 0 - || not (Array.for_all (fun arr -> Array.length arr = const_len) attr_arrays) - then - None - else - let mid = const_len / 2 in - let e_aligned = - let check i = - let e = const_arr.(i).e in - Array.for_all (fun arr -> arr.(i).e = e) attr_arrays - in - check 0 && check mid && check (const_len - 1) - in - if not e_aligned then - None - else - let base_e = const_arr.(0).e in - let dense = - const_arr.(const_len - 1).e = base_e + const_len - 1 - && Array.for_all - (fun arr -> - arr.(0).e = base_e && arr.(const_len - 1).e = base_e + const_len - 1) - attr_arrays - in - let specialized_find = - let expected = e_var :: (value_attrs |> Array.to_list |> List.map fst) in - attrs = expected - in - if not (specialized_find && dense) then - None - else - let rows = ref [] in - (match attr_count with - | 4 -> - let a0 = attr_arrays.(0) in - let a1 = attr_arrays.(1) in - let a2 = attr_arrays.(2) in - let a3 = attr_arrays.(3) in - (match avet_ids_array const_attr const_value with - | Some ids -> - for i = Array.length ids - 1 downto 0 do - let e = ids.(i) in - let index = e - base_e in - if index >= 0 && index < const_len then - rows := - [ Result_entity e - ; Result_value a0.(index).v - ; Result_value a1.(index).v - ; Result_value a2.(index).v - ; Result_value a3.(index).v - ] - :: !rows - done - | None -> - for i = const_len - 1 downto 0 do - if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 then - let e = const_arr.(i).e in - rows := - [ Result_entity e - ; Result_value a0.(i).v - ; Result_value a1.(i).v - ; Result_value a2.(i).v - ; Result_value a3.(i).v - ] - :: !rows - done) - | 1 -> - let a0 = attr_arrays.(0) in - (match avet_ids_array const_attr const_value with - | Some ids -> - for i = Array.length ids - 1 downto 0 do - let e = ids.(i) in - let index = e - base_e in - if index >= 0 && index < const_len then - rows := - [ Result_entity e; Result_value a0.(index).v ] :: !rows - done - | None -> - for i = const_len - 1 downto 0 do - if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 then - rows := - [ Result_entity const_arr.(i).e; Result_value a0.(i).v ] :: !rows - done) - | _ -> - (match avet_ids_array const_attr const_value with - | Some ids -> - for i = Array.length ids - 1 downto 0 do - let e = ids.(i) in - let index = e - base_e in - if index >= 0 && index < const_len then - let rec vals a acc = - if a < 0 then Result_entity e :: acc - else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) - in - rows := vals (attr_count - 1) [] :: !rows - done - | None -> - for i = const_len - 1 downto 0 do - if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 then - let e = const_arr.(i).e in - let rec vals a acc = - if a < 0 then Result_entity e :: acc - else vals (a - 1) (Result_value attr_arrays.(a).(i).v :: acc) - in - rows := vals (attr_count - 1) [] :: !rows - done)); - let unique_rows = - (not source_db.history) - && source_db.duplicate_datoms = [] - && List.mem e_var attrs - in - Some { attrs; rows = !rows; lookup_vars; unique_rows }) - | _ -> None - in - match try_same_entity_constant_dense_rows with - | Some relation -> Some relation - | None -> - let try_not_single_value_aevt_scan = - if not has_not then - None - else - match value_var_patterns, constant_patterns, required_patterns, excluded_patterns, relation_comparisons with - | [ (value_var, seed_attr) ], [], [], [ (_, clause_attr, QValue clause_value) ], [] - when not (query_evaluator_context.is_reverse_ref seed_attr) - && not (query_evaluator_context.is_reverse_ref clause_attr) -> - (match aevt_attr_array source_db seed_attr with - | None -> None - | Some seed_arr -> - let max_entity = source_db.max_datom_e + 1 in - let excluded = Bytes.make max_entity '\000' in - let mark_excluded entity_id = - if entity_id >= 0 && entity_id < max_entity then - Bytes.unsafe_set excluded entity_id '\001' - in - (match entity_ids_by_attr_value source_db clause_attr clause_value with - | Some entity_ids -> List.iter mark_excluded entity_ids - | None -> - datoms_by_attr_value source_db clause_attr clause_value - |> List.iter (fun datom -> mark_excluded datom.e)); - let rows = ref [] in - let emit datom = - if - datom.e >= 0 - && datom.e < max_entity - && Bytes.unsafe_get excluded datom.e = '\000' - then - let value = Query.result_of_datom_v datom in - match attrs with - | [ entity_attr; value_attr ] - when entity_attr = e_var && value_attr = value_var -> - rows := [ Result_entity datom.e; value ] :: !rows - | [ value_attr; entity_attr ] - when entity_attr = e_var && value_attr = value_var -> - rows := [ value; Result_entity datom.e ] :: !rows - | _ -> - (match binding_row attrs [ e_var, Result_entity datom.e; value_var, value ] with - | Some row -> rows := row :: !rows - | None -> ()) - in - for i = Array.length seed_arr - 1 downto 0 do - emit seed_arr.(i) - done; - (match aevt_duplicate_datoms source_db seed_attr with - | [] -> () - | duplicates -> List.iter emit duplicates); - let unique_rows = - (not source_db.history) - && source_db.duplicate_datoms = [] - && List.mem e_var attrs - && cardinality_one source_db seed_attr - in - Some { attrs; rows = !rows; lookup_vars; unique_rows }) - | _ -> None - in - match try_not_single_value_aevt_scan with - | Some relation -> Some relation - | None -> - let source_context = query_source_context db in - let direct_attr attr = - not (query_evaluator_context.is_reverse_ref attr) - in - let datoms_matching attr value = - if - direct_attr attr && query_value_uses_avet value - && query_attr_uses_avet source_db attr - then - datoms_by_attr_value source_db attr value - else - let datoms = source_context.pattern_datoms source_db (QVar e_var) (QAttr attr) (QValue value) None in - if direct_attr attr then - List.of_seq datoms - else - datoms - |> Seq.filter (fun datom -> - Option.is_some - (source_context.match_data_pattern source_db [] (QVar e_var) (QAttr attr) (QValue value) datom)) - |> List.of_seq - in - let avet_entity_ids attr value = - if direct_attr attr && query_value_uses_avet value && query_attr_uses_avet source_db attr then - entity_ids_by_attr_value source_db attr value - else - None - in - let avet_entity_ids_array attr value = - if direct_attr attr && query_value_uses_avet value && query_attr_uses_avet source_db attr then - entity_ids_array_by_attr_value source_db attr value - else - None - in - let constant_datoms = - constant_patterns - |> List.map (fun (attr, value) -> attr, value, lazy (datoms_matching attr value)) - in - if - List.exists - (fun (attr, value, datoms) -> - match avet_entity_ids attr value with - | Some [] -> true - | Some _ -> false - | None -> Lazy.force datoms = []) - constant_datoms - then - Some { attrs; rows = []; lookup_vars; unique_rows = true } - else - let avet_single_entity_rows = - match constant_patterns, value_var_patterns, required_patterns, excluded_patterns, relation_comparisons with - | [ (attr, value) ], [], [], [], [] -> ( - match avet_entity_ids attr value with - | Some entity_ids -> Some (List.map (fun entity_id -> [ Result_entity entity_id ]) entity_ids) - | None -> None) - | _ -> None - in - if Option.is_some avet_single_entity_rows then - Some - { attrs - ; rows = Option.get avet_single_entity_rows - ; lookup_vars - ; unique_rows = true - } - else - let set_from_entity_ids entity_ids = - let entities = Bytes.make (source_db.max_datom_e + 1) '\000' in - List.iter - (fun entity_id -> - if entity_id >= 0 && entity_id < Bytes.length entities then - Bytes.unsafe_set entities entity_id '\001') - entity_ids; - entities - in - let set_from_datoms datoms = - let entities = Bytes.make (source_db.max_datom_e + 1) '\000' in - List.iter - (fun datom -> - if datom.e >= 0 && datom.e < Bytes.length entities then - Bytes.unsafe_set entities datom.e '\001') - datoms; - entities - in - (* Defer (max_e+1) constant bitsets until a fallback path needs them. - Dense AVET→AEVT gathers only need entity id arrays. *) - let constant_sets = - lazy - (constant_datoms - |> List.map (fun (attr, value, datoms) -> - match avet_entity_ids attr value with - | Some entity_ids -> set_from_entity_ids entity_ids - | None -> set_from_datoms (Lazy.force datoms))) - in - let constant_count (attr, value, datoms) = - match avet_entity_ids attr value with - | Some entity_ids -> List.length entity_ids - | None -> List.length (Lazy.force datoms) - in - let candidate_entities () = - match constant_datoms with - | [] -> - (match value_var_patterns, required_patterns with - | (_, attr) :: _, _ | [], attr :: _ -> - source_context.pattern_datoms source_db (QVar e_var) (QAttr attr) QWildcard None - |> Seq.map (fun datom -> datom.e) - |> List.of_seq - | [], [] -> []) - | datoms_by_constant -> - datoms_by_constant - |> List.sort (fun left right -> compare (constant_count left) (constant_count right)) - |> function - | (attr, value, datoms) :: _ -> ( - match avet_entity_ids attr value with - | Some entity_ids -> entity_ids - | None -> List.map (fun datom -> datom.e) (Lazy.force datoms)) - | [] -> [] - in - let has_pattern entity_id attr value_term = - let datoms = source_context.pattern_datoms source_db (QEntity entity_id) (QAttr attr) value_term None in - if direct_attr attr then - Option.is_some (Seq.uncons datoms) - else - datoms - |> Seq.exists (fun datom -> - Option.is_some - (source_context.match_data_pattern source_db [] (QEntity entity_id) (QAttr attr) value_term datom)) - in - let excluded_sets = - excluded_patterns - |> List.map (fun (_, attr, value_term) -> - match value_term with - | QValue value - when direct_attr attr && query_value_uses_avet value && query_attr_uses_avet source_db attr -> ( - match avet_entity_ids attr value with - | Some entity_ids -> set_from_entity_ids entity_ids - | None -> - let entities = Bytes.make (source_db.max_datom_e + 1) '\000' in - datoms_matching attr value - |> List.iter (fun datom -> - if datom.e >= 0 && datom.e < Bytes.length entities then - Bytes.unsafe_set entities datom.e '\001'); - entities) - | _ -> - let entities = Bytes.make (source_db.max_datom_e + 1) '\000' in - let datoms = source_context.pattern_datoms source_db (QVar e_var) (QAttr attr) value_term None in - let mark datom = - if datom.e >= 0 && datom.e < Bytes.length entities then - Bytes.unsafe_set entities datom.e '\001' - in - if direct_attr attr then - datoms |> Seq.iter mark - else - datoms - |> Seq.iter (fun datom -> - if - Option.is_some - (source_context.match_data_pattern source_db [] (QVar e_var) (QAttr attr) value_term datom) - then - mark datom); - entities) - in - let matches_required = - match required_patterns with - | [] -> fun _ -> true - | [ attr ] -> fun entity_id -> has_pattern entity_id attr QWildcard - | patterns -> - fun entity_id -> - patterns |> List.for_all (fun attr -> has_pattern entity_id attr QWildcard) - in - let constant_matches entity_id = - Lazy.force constant_sets - |> List.for_all (fun entities -> - entity_id >= 0 - && entity_id < Bytes.length entities - && Bytes.unsafe_get entities entity_id = '\001') - in - let matches_constants = - match constant_patterns with - | [] -> fun _ -> true - | [ _ ] -> - (* Prefer AVET id membership via candidate_entities / dense emit; when a - fallback still consults the bitset, build it once. *) - fun entity_id -> - (match Lazy.force constant_sets with - | [ entities ] -> - entity_id >= 0 - && entity_id < Bytes.length entities - && Bytes.unsafe_get entities entity_id = '\001' - | _ -> constant_matches entity_id) - | [ _; _ ] -> - fun entity_id -> - (match Lazy.force constant_sets with - | [ left; right ] -> - entity_id >= 0 - && entity_id < Bytes.length left - && Bytes.unsafe_get left entity_id = '\001' - && entity_id < Bytes.length right - && Bytes.unsafe_get right entity_id = '\001' - | _ -> constant_matches entity_id) - | _ -> constant_matches - in - let matches_excluded = - match excluded_sets with - | [] -> fun _ -> false - | [ entities ] -> - fun entity_id -> - entity_id >= 0 - && entity_id < Bytes.length entities - && Bytes.unsafe_get entities entity_id = '\001' - | sets -> - fun entity_id -> - sets - |> List.exists (fun entities -> - entity_id >= 0 - && entity_id < Bytes.length entities - && Bytes.unsafe_get entities entity_id = '\001') - in - let entity_allowed = - match excluded_sets, constant_patterns with - | [], [] -> fun entity_id -> matches_required entity_id - | [], [ _ ] -> - (* Single constant: dense/AVET paths filter membership; required-only here. *) - fun entity_id -> matches_required entity_id && matches_constants entity_id - | [], _ -> fun entity_id -> matches_constants entity_id && matches_required entity_id - | _, _ -> - fun entity_id -> - matches_constants entity_id && matches_required entity_id && not (matches_excluded entity_id) - in - let value_result_of_datom datom = - Query.result_of_ref (Query.result_of_datom_v datom) - in - let value_results entity_id attr = - let datoms = source_context.pattern_datoms source_db (QEntity entity_id) (QAttr attr) QWildcard None in - if direct_attr attr then - datoms |> Seq.map (fun datom -> result_of_pattern_position datom 2) |> List.of_seq - else - datoms - |> Seq.filter_map (fun datom -> - let* _ = - source_context.match_data_pattern source_db [] (QEntity entity_id) (QAttr attr) QWildcard datom - in - Some (result_of_pattern_position datom 2)) - |> List.of_seq - in - let single_value_result entity_id attr = - if direct_attr attr then - find_entity_attr_value source_db entity_id attr - else - let datoms = - source_context.pattern_datoms source_db (QEntity entity_id) (QAttr attr) QWildcard None - in - datoms - |> Seq.find_map (fun datom -> - let* _ = - source_context.match_data_pattern source_db [] (QEntity entity_id) (QAttr attr) QWildcard datom - in - Some (result_of_pattern_position datom 2)) - in - let extend_bindings bindings (value_var, attr) = - bindings - |> List.concat_map (fun binding -> - let entity_id = - match List.assoc e_var binding with - | Result_entity entity_id -> entity_id - | _ -> -1 - in - let values = value_results entity_id attr in - values - |> List.filter_map (fun value -> - match List.assoc_opt value_var binding with - | Some existing when existing = value -> Some binding - | Some _ -> None - | None -> Some ((value_var, value) :: binding))) - in - let gather_slots_for attrs value_vars = - let var_index = - let table = Hashtbl.create (List.length value_vars) in - List.iteri (fun index (value_var, _) -> Hashtbl.replace table value_var index) value_vars; - table - in - attrs - |> List.fold_left - (fun slots attr -> - match slots with - | None -> None - | Some slots -> - if attr = e_var then - Some (`Gather_entity :: slots) - else - match Hashtbl.find_opt var_index attr with - | Some index -> Some (`Gather_value index :: slots) - | None -> None) - (Some []) - |> Option.map (fun slots -> Array.of_list (List.rev slots)) - in - let build_row_from_slots slots entity_id value_results = - let slot_count = Array.length slots in - let rec loop i acc = - if i < 0 then acc - else - match slots.(i) with - | `Gather_entity -> loop (i - 1) (Result_entity entity_id :: acc) - | `Gather_value index -> loop (i - 1) (value_results.(index) :: acc) - in - loop (slot_count - 1) [] - in - (* Dense / binary-search gather: fill card-one value attrs from AEVT arrays - without per-entity pattern_datoms Seq. *) - let rows_from_dense_aevt_gather value_vars = - (* value_var_patterns is reverse-cons'd; restore pattern/attrs order. *) - let value_vars = List.rev value_vars in - if - value_vars = [] - || not - (List.for_all - (fun (_, attr) -> direct_attr attr && cardinality_one source_db attr) - value_vars) - then - None - else - let value_attr_arrays = - value_vars - |> List.map (fun (value_var, attr) -> - match aevt_attr_array source_db attr with - | None -> None - | Some arr -> Some (value_var, arr)) - in - if List.exists Option.is_none value_attr_arrays then - None - else - let value_attrs = value_attr_arrays |> List.map Option.get |> Array.of_list in - let attr_count = Array.length value_attrs in - let attr_arrays = Array.map (fun (_, arr) -> arr) value_attrs in - match gather_slots_for attrs value_vars with - | None -> None - | Some row_slots -> - let specialized_find = - let expected = e_var :: (value_attrs |> Array.to_list |> List.map fst) in - attrs = expected - in - let value_results = Array.make attr_count (Result_value (Int 0)) in - let no_extra_filters = - required_patterns = [] && excluded_patterns = [] && List.length constant_patterns <= 1 - in - (* Const-first aligned gather (old aligned_constant_rows): use constant attr - AEVT array as alignment reference — required for q-5-merge where the - constant attr array may share length but differs from value-array base_e. *) - let try_const_arr_aligned_rows = - match constant_patterns with - | [ (const_attr, const_value) ] when specialized_find -> - (match aevt_attr_array source_db const_attr with - | None -> None - | Some const_arr -> - let const_len = Array.length const_arr in - if const_len = 0 then None - else if not (Array.for_all (fun arr -> Array.length arr = const_len) attr_arrays) - then - None - else - let mid = const_len / 2 in - let e_aligned = - let check i = - let e = const_arr.(i).e in - Array.for_all (fun arr -> arr.(i).e = e) attr_arrays - in - check 0 && check mid && check (const_len - 1) - in - if not e_aligned then - None - else - let base_e = const_arr.(0).e in - let dense = - const_arr.(const_len - 1).e = base_e + const_len - 1 - && Array.for_all - (fun arr -> - arr.(0).e = base_e && arr.(const_len - 1).e = base_e + const_len - 1) - attr_arrays - in - if not dense then - None - else - let rows = ref [] in - (match attr_count with - | 4 -> - let a0 = attr_arrays.(0) in - let a1 = attr_arrays.(1) in - let a2 = attr_arrays.(2) in - let a3 = attr_arrays.(3) in - (match avet_entity_ids_array const_attr const_value with - | Some ids -> - for i = Array.length ids - 1 downto 0 do - let e = ids.(i) in - let index = e - base_e in - if index >= 0 && index < const_len then - rows := - [ Result_entity e - ; Result_value a0.(index).v - ; Result_value a1.(index).v - ; Result_value a2.(index).v - ; Result_value a3.(index).v - ] - :: !rows - done - | None -> - for i = const_len - 1 downto 0 do - if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 - then - let e = const_arr.(i).e in - rows := - [ Result_entity e - ; Result_value a0.(i).v - ; Result_value a1.(i).v - ; Result_value a2.(i).v - ; Result_value a3.(i).v - ] - :: !rows - done) - | 1 -> - let a0 = attr_arrays.(0) in - (match avet_entity_ids_array const_attr const_value with - | Some ids -> - for i = Array.length ids - 1 downto 0 do - let e = ids.(i) in - let index = e - base_e in - if index >= 0 && index < const_len then - rows := - [ Result_entity e; Result_value a0.(index).v ] :: !rows - done - | None -> - for i = const_len - 1 downto 0 do - if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 - then - rows := - [ Result_entity const_arr.(i).e; Result_value a0.(i).v ] :: !rows - done) - | _ -> - (match avet_entity_ids_array const_attr const_value with - | Some ids -> - for i = Array.length ids - 1 downto 0 do - let e = ids.(i) in - let index = e - base_e in - if index >= 0 && index < const_len then - let rec vals a acc = - if a < 0 then Result_entity e :: acc - else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) - in - rows := vals (attr_count - 1) [] :: !rows - done - | None -> - for i = const_len - 1 downto 0 do - if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 - then - let e = const_arr.(i).e in - let rec vals a acc = - if a < 0 then Result_entity e :: acc - else vals (a - 1) (Result_value attr_arrays.(a).(i).v :: acc) - in - rows := vals (attr_count - 1) [] :: !rows - done)); - Some !rows) - | _ -> None - in - match try_const_arr_aligned_rows with - | Some rows -> Some rows - | None -> - let dense_base = - if attr_count = 0 then None - else - let first = attr_arrays.(0) in - let len = Array.length first in - if len = 0 then None - else if not (Array.for_all (fun arr -> Array.length arr = len) attr_arrays) then - None - else - let base_e = first.(0).e in - let last_e = first.(len - 1).e in - if last_e <> base_e + len - 1 then None - else - let mid = len / 2 in - let aligned = - let check i = - let e = first.(i).e in - Array.for_all (fun arr -> arr.(i).e = e) attr_arrays - in - check 0 && check mid && check (len - 1) - in - if aligned then Some (base_e, len) else None - in - match dense_base, constant_patterns with - | Some (base_e, dense_len), [ (const_attr, const_value) ] -> ( - match aevt_attr_array source_db const_attr with - | Some const_arr - when Array.length const_arr = dense_len - && const_arr.(0).e = base_e - && const_arr.(dense_len - 1).e = base_e + dense_len - 1 -> - let rows = ref [] in - let emit_at index = - let e = base_e + index in - if no_extra_filters || entity_allowed e then - if specialized_find then ( - match attr_count with - | 1 -> - rows := - [ Result_entity e; Result_value attr_arrays.(0).(index).v ] - :: !rows - | 2 -> - rows := - [ Result_entity e - ; Result_value attr_arrays.(0).(index).v - ; Result_value attr_arrays.(1).(index).v - ] - :: !rows - | 4 -> - rows := - [ Result_entity e - ; Result_value attr_arrays.(0).(index).v - ; Result_value attr_arrays.(1).(index).v - ; Result_value attr_arrays.(2).(index).v - ; Result_value attr_arrays.(3).(index).v - ] - :: !rows - | _ -> - let rec vals a acc = - if a < 0 then Result_entity e :: acc - else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) - in - rows := vals (attr_count - 1) [] :: !rows) - else ( - for a = 0 to attr_count - 1 do - value_results.(a) <- value_result_of_datom attr_arrays.(a).(index) - done; - rows := build_row_from_slots row_slots e value_results :: !rows) - in - (match avet_entity_ids_array const_attr const_value with - | Some ids -> - for i = Array.length ids - 1 downto 0 do - let e = ids.(i) in - let index = e - base_e in - if index >= 0 && index < dense_len then emit_at index - done - | None -> - for i = dense_len - 1 downto 0 do - if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 then - emit_at i - done); - Some !rows - | _ -> - let rows = ref [] in - let emit entity_id = - let index = entity_id - base_e in - if - index >= 0 && index < dense_len - && (no_extra_filters || entity_allowed entity_id) - then - if specialized_find then - match attr_count with - | 1 -> - rows := - [ Result_entity entity_id; Result_value attr_arrays.(0).(index).v ] - :: !rows - | 2 -> - rows := - [ Result_entity entity_id - ; Result_value attr_arrays.(0).(index).v - ; Result_value attr_arrays.(1).(index).v - ] - :: !rows - | 4 -> - rows := - [ Result_entity entity_id - ; Result_value attr_arrays.(0).(index).v - ; Result_value attr_arrays.(1).(index).v - ; Result_value attr_arrays.(2).(index).v - ; Result_value attr_arrays.(3).(index).v - ] - :: !rows - | _ -> - let rec vals a acc = - if a < 0 then Result_entity entity_id :: acc - else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) - in - rows := vals (attr_count - 1) [] :: !rows - else ( - for a = 0 to attr_count - 1 do - value_results.(a) <- value_result_of_datom attr_arrays.(a).(index) - done; - rows := build_row_from_slots row_slots entity_id value_results :: !rows) - in - (match avet_entity_ids_array const_attr const_value with - | Some ids -> - for i = Array.length ids - 1 downto 0 do - emit ids.(i) - done; - Some !rows - | None -> - List.iter emit (candidate_entities ()); - Some (List.rev !rows))) - | Some (base_e, dense_len), _ -> - let rows = ref [] in - let emit entity_id = - let index = entity_id - base_e in - if - index >= 0 && index < dense_len - && (no_extra_filters || entity_allowed entity_id) - then - if specialized_find then - match attr_count with - | 1 -> - rows := - [ Result_entity entity_id; Result_value attr_arrays.(0).(index).v ] - :: !rows - | 2 -> - rows := - [ Result_entity entity_id - ; Result_value attr_arrays.(0).(index).v - ; Result_value attr_arrays.(1).(index).v - ] - :: !rows - | 4 -> - rows := - [ Result_entity entity_id - ; Result_value attr_arrays.(0).(index).v - ; Result_value attr_arrays.(1).(index).v - ; Result_value attr_arrays.(2).(index).v - ; Result_value attr_arrays.(3).(index).v - ] - :: !rows - | _ -> - let rec vals a acc = - if a < 0 then Result_entity entity_id :: acc - else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) - in - rows := vals (attr_count - 1) [] :: !rows - else ( - for a = 0 to attr_count - 1 do - value_results.(a) <- value_result_of_datom attr_arrays.(a).(index) - done; - rows := build_row_from_slots row_slots entity_id value_results :: !rows) - in - List.iter emit (candidate_entities ()); - Some (List.rev !rows) - | None, _ -> - let entity_ids = candidate_entities () in - let rows = - entity_ids - |> List.filter_map (fun entity_id -> - if not (entity_allowed entity_id) then None - else - let rec fill a = - if a >= attr_count then true - else - match find_entity_in_aevt_array attr_arrays.(a) entity_id with - | None -> false - | Some datom -> - value_results.(a) <- value_result_of_datom datom; - fill (a + 1) - in - if not (fill 0) then None - else if specialized_find then - let rec vals a acc = - if a < 0 then Result_entity entity_id :: acc - else vals (a - 1) (value_results.(a) :: acc) - in - Some (vals (attr_count - 1) []) - else - Some (build_row_from_slots row_slots entity_id value_results)) - in - Some rows - in - let rows_from_cardinality_one_candidates value_vars = - match rows_from_dense_aevt_gather value_vars with - | Some rows -> rows - | None -> - candidate_entities () - |> List.filter_map (fun entity_id -> - if not (entity_allowed entity_id) then - None - else - let* binding = - value_vars - |> List.fold_left - (fun binding (value_var, attr) -> - match binding with - | None -> None - | Some binding -> - single_value_result entity_id attr - |> Option.map (fun value -> (value_var, value) :: binding)) - (Some [ e_var, Result_entity entity_id ]) - in - binding_row attrs binding) - in - let rows_from_aevt_array_scan scan_value_var scan_attr remaining_value_vars = - match remaining_value_vars, aevt_attr_array source_db scan_attr with - | [], Some scan_arr when direct_attr scan_attr -> ( - match attrs with - | [ entity_attr; value_attr ] - when entity_attr = e_var && value_attr = scan_value_var -> - let rows = ref [] in - for i = Array.length scan_arr - 1 downto 0 do - let datom = scan_arr.(i) in - if entity_allowed datom.e then - rows := - [ Result_entity datom.e; value_result_of_datom datom ] :: !rows - done; - Some !rows - | [ value_attr; entity_attr ] - when entity_attr = e_var && value_attr = scan_value_var -> - let rows = ref [] in - for i = Array.length scan_arr - 1 downto 0 do - let datom = scan_arr.(i) in - if entity_allowed datom.e then - rows := - [ value_result_of_datom datom; Result_entity datom.e ] :: !rows - done; - Some !rows - | _ -> - match gather_slots_for attrs [ (scan_value_var, scan_attr) ] with - | None -> None - | Some row_slots -> - let value_results = [| Result_value (Int 0) |] in - let rows = ref [] in - for i = Array.length scan_arr - 1 downto 0 do - let datom = scan_arr.(i) in - if entity_allowed datom.e then ( - value_results.(0) <- value_result_of_datom datom; - rows := build_row_from_slots row_slots datom.e value_results :: !rows) - done; - Some !rows) - | _ :: _, Some _ - when List.for_all - (fun (_, attr) -> direct_attr attr && cardinality_one source_db attr) - ((scan_value_var, scan_attr) :: remaining_value_vars) -> - rows_from_dense_aevt_gather ((scan_value_var, scan_attr) :: remaining_value_vars) - | _ -> None - in - let rows_from_cardinality_one_value_scan scan_value_var scan_attr remaining_value_vars = - match rows_from_aevt_array_scan scan_value_var scan_attr remaining_value_vars with - | Some rows -> rows - | None -> - let direct_allowed_entity_set () = - match Lazy.force constant_sets with - | [] | [ _ ] -> None - | first :: rest -> - let allowed = Bytes.copy first in - for index = 0 to Bytes.length allowed - 1 do - if - Bytes.unsafe_get allowed index = '\001' - && List.exists (fun entities -> Bytes.unsafe_get entities index <> '\001') rest - then - Bytes.unsafe_set allowed index '\000' - done; - Some allowed - in - match remaining_value_vars, attrs, Lazy.force constant_sets with - | [], [ entity_attr; value_attr ], _ :: _ :: _ - when direct_attr scan_attr && entity_attr = e_var && value_attr = scan_value_var -> - let scan_datoms = source_context.pattern_datoms source_db (QVar e_var) (QAttr scan_attr) QWildcard None in - let allowed = direct_allowed_entity_set () in - let entity_allowed = - match allowed with - | Some allowed -> - fun entity_id -> - entity_id >= 0 - && entity_id < Bytes.length allowed - && Bytes.get allowed entity_id = '\001' - && matches_required entity_id - | None -> entity_allowed - in - if is_ref_attr source_db scan_attr then - let rec collect acc seq = - match seq () with - | Seq.Nil -> List.rev acc - | Seq.Cons (scan_datom, rest) -> - if entity_allowed scan_datom.e then - collect ([ Result_entity scan_datom.e; result_of_pattern_position scan_datom 2 ] :: acc) rest - else - collect acc rest - in - collect [] scan_datoms - else - let rec collect acc seq = - match seq () with - | Seq.Nil -> List.rev acc - | Seq.Cons (scan_datom, rest) -> - if entity_allowed scan_datom.e then - collect ([ Result_entity scan_datom.e; result_of_pattern_position scan_datom 2 ] :: acc) rest - else - collect acc rest - in - collect [] scan_datoms - | [], [ value_attr; entity_attr ], _ :: _ :: _ - when direct_attr scan_attr && entity_attr = e_var && value_attr = scan_value_var -> - let scan_datoms = source_context.pattern_datoms source_db (QVar e_var) (QAttr scan_attr) QWildcard None in - let allowed = direct_allowed_entity_set () in - let entity_allowed = - match allowed with - | Some allowed -> - fun entity_id -> - entity_id >= 0 - && entity_id < Bytes.length allowed - && Bytes.get allowed entity_id = '\001' - && matches_required entity_id - | None -> entity_allowed - in - if is_ref_attr source_db scan_attr then - let rec collect acc seq = - match seq () with - | Seq.Nil -> List.rev acc - | Seq.Cons (scan_datom, rest) -> - if entity_allowed scan_datom.e then - collect ([ result_of_pattern_position scan_datom 2; Result_entity scan_datom.e ] :: acc) rest - else - collect acc rest - in - collect [] scan_datoms - else - let rec collect acc seq = - match seq () with - | Seq.Nil -> List.rev acc - | Seq.Cons (scan_datom, rest) -> - if entity_allowed scan_datom.e then - collect ([ result_of_pattern_position scan_datom 2; Result_entity scan_datom.e ] :: acc) rest - else - collect acc rest - in - collect [] scan_datoms - | _ -> - let scan_datoms = - source_context.pattern_datoms source_db (QVar e_var) (QAttr scan_attr) QWildcard None - in - scan_datoms - |> Seq.filter_map (fun scan_datom -> - if not (entity_allowed scan_datom.e) then - None - else - let binding = - (scan_value_var, result_of_pattern_position scan_datom 2) - :: [ e_var, Result_entity scan_datom.e ] - in - let* binding = - remaining_value_vars - |> List.fold_left - (fun binding (value_var, attr) -> - match binding with - | None -> None - | Some binding -> - single_value_result scan_datom.e attr - |> Option.map (fun value -> (value_var, value) :: binding)) - (Some binding) - in - binding_row attrs binding) - |> List.of_seq - in - let compute_default_rows () = - match value_var_patterns with - | value_vars - when constant_patterns <> [] - && value_vars <> [] - && List.for_all - (fun (_, attr) -> direct_attr attr && cardinality_one source_db attr) - value_vars -> ( - match rows_from_dense_aevt_gather value_vars with - | Some rows -> rows - | None -> rows_from_cardinality_one_candidates value_vars) - | (scan_value_var, scan_attr) :: remaining_value_vars - when constant_patterns <> [] - && List.length value_var_patterns >= 2 - && List.for_all (fun (_, attr) -> cardinality_one source_db attr) value_var_patterns -> - rows_from_cardinality_one_value_scan scan_value_var scan_attr remaining_value_vars - | _ :: _ - when constant_patterns <> [] - && List.for_all (fun (_, attr) -> cardinality_one source_db attr) value_var_patterns -> - rows_from_cardinality_one_candidates value_var_patterns - | (scan_value_var, scan_attr) :: remaining_value_vars - when direct_attr scan_attr - && List.for_all - (fun (_, attr) -> direct_attr attr && cardinality_one source_db attr) - remaining_value_vars -> - rows_from_cardinality_one_value_scan scan_value_var scan_attr remaining_value_vars - | (scan_value_var, scan_attr) :: remaining_value_vars - when List.for_all (fun (_, attr) -> cardinality_one source_db attr) value_var_patterns -> - rows_from_cardinality_one_value_scan scan_value_var scan_attr remaining_value_vars - | _ :: _ when List.for_all (fun (_, attr) -> cardinality_one source_db attr) value_var_patterns -> - rows_from_cardinality_one_candidates value_var_patterns - | _ -> - candidate_entities () - |> List.sort_uniq compare - |> List.concat_map (fun entity_id -> - if not (entity_allowed entity_id) then - [] - else - let bindings = - value_var_patterns - |> List.fold_left - (fun bindings value_pattern -> - match bindings with - | [] -> [] - | bindings -> extend_bindings bindings value_pattern) - [ [ e_var, Result_entity entity_id ] ] - in - bindings |> List.filter_map (binding_row attrs)) - in - let rows = - match constant_patterns, value_var_patterns, required_patterns, excluded_patterns, relation_comparisons with - | [ (attr, value) ], [], [], [], [] when value_var_patterns = [] -> ( - match avet_entity_ids attr value with - | Some entity_ids -> List.map (fun entity_id -> [ Result_entity entity_id ]) entity_ids - | None -> compute_default_rows ()) - | _ -> compute_default_rows () - in - let unique_rows = - (not source_db.history) - && source_db.duplicate_datoms = [] - && List.mem e_var attrs - && List.for_all (fun (_, attr) -> cardinality_one source_db attr) value_var_patterns + let pattern_relation (_, attr, value_term) = + relation_of_pattern db source [ QVar e_var; QAttr attr; value_term ] in - let relation = { attrs; rows; lookup_vars; unique_rows } in - Some - (List.fold_left - (fun relation -> function - | ComparisonPredicate (predicate, left_term, right_term) -> - filter_relation_comparison db relation predicate left_term right_term - | _ -> relation) - relation - relation_comparisons))) + (match + patterns + |> List.filter_map pattern_relation + |> function + | [] -> None + | first :: rest -> Some (List.fold_left hash_join first rest) + with + | None -> None + | Some relation -> + let relation = + excluded_patterns + |> List.fold_left + (fun relation (_, attr, value_term) -> + match relation_of_pattern db source [ QVar e_var; QAttr attr; value_term ] with + | Some excluded -> Option.value (anti_join relation excluded) ~default:relation + | None -> relation) + relation + in + let unique_rows = + (not source_db.history) + && source_db.duplicate_datoms = [] + && List.mem e_var attrs + in + let relation = { relation with lookup_vars; unique_rows } in + Some + (List.fold_left + (fun relation -> function + | ComparisonPredicate (predicate, left_term, right_term) -> + filter_relation_comparison db relation predicate left_term right_term + | _ -> relation) + relation + relation_comparisons)))) | _ -> None let relation_of_cross_entity_value_join _db source clauses = @@ -3660,368 +2550,6 @@ end) = struct in Some { attrs = first.attrs; rows; lookup_vars; unique_rows } - let ensure_sorted_entity_ids ids = - match ids with - | [] | [ _ ] -> ids - | first :: rest -> - let rec ascending prev = function - | [] -> true - | x :: xs -> x >= prev && ascending x xs - in - if ascending first rest then ids else List.sort_uniq compare ids - - let intersect_sorted_entity_id_lists left right = - let rec loop left right acc = - match left, right with - | [], _ | _, [] -> List.rev acc - | x :: xs, y :: ys -> - if x = y then loop xs ys (x :: acc) - else if x < y then loop xs right acc - else loop left ys acc - in - loop left right [] - - let intersect_constant_entity_ids id_lists = - let id_lists = List.map ensure_sorted_entity_ids id_lists in - match List.sort (fun left right -> compare (List.length left) (List.length right)) id_lists with - | [] -> [] - | smallest :: rest -> List.fold_left intersect_sorted_entity_id_lists smallest rest - - (** Lightweight same-entity fast paths for eval_relation_rows. *) - let try_fast_empty_relation_rows _db default_source clauses = - match default_source with - | Db_source source_db -> - let direct_attr attr = not (query_evaluator_context.is_reverse_ref attr) in - let unique_rows_flag attrs e_var = - (not source_db.history) - && source_db.duplicate_datoms = [] - && List.mem e_var attrs - in - let parse_same_entity_clauses () = - let rec parse acc excluded = function - | [] -> Some (List.rev acc, List.rev excluded) - | Pattern (QVar e_var, QAttr attr, value_term) :: rest -> ( - match acc with - | [] -> parse ((e_var, attr, value_term) :: acc) excluded rest - | (e, _, _) :: _ when e = e_var -> parse ((e_var, attr, value_term) :: acc) excluded rest - | _ -> None) - | Not [ Pattern (QVar e_var, QAttr attr, QValue value) ] :: rest -> ( - match acc with - | (e, _, _) :: _ when e = e_var -> parse acc ((attr, value) :: excluded) rest - | _ -> None) - | NotJoin ([ join_e ], [ Pattern (QVar e_var, QAttr attr, QValue value) ]) :: rest - when join_e = e_var -> ( - match acc with - | (e, _, _) :: _ when e = e_var -> parse acc ((attr, value) :: excluded) rest - | _ -> None) - | _ -> None - in - parse [] [] clauses - in - (match parse_same_entity_clauses () with - | None -> None - | Some (patterns, excluded_patterns) -> - let attrs = - patterns - |> List.concat_map (fun (e_var, attr, value_term) -> [ QVar e_var; QAttr attr; value_term ]) - |> unique_vars - in - let (e_var, _, _) = List.hd patterns in - if not (List.for_all (fun (candidate, _, _) -> candidate = e_var) patterns) then - None - else - let value_var_patterns, constant_patterns, required_patterns = - patterns - |> List.fold_left - (fun (value_vars, constants, required) (_, attr, value_term) -> - match value_term with - | QVar value_var when value_var <> e_var -> - ((value_var, attr) :: value_vars, constants, required) - | QValue value -> (value_vars, (attr, value) :: constants, required) - | QWildcard -> (value_vars, constants, attr :: required) - | QVar _ | QEntity _ | QAttr _ | QIdent _ | QLookupRef _ | QSource _ -> - (value_vars, constants, required)) - ([], [], []) - in - let duplicate_value_var = - let seen = Hashtbl.create (List.length value_var_patterns) in - List.exists - (fun (value_var, _) -> - if Hashtbl.mem seen value_var then true - else ( - Hashtbl.add seen value_var (); - false )) - value_var_patterns - in - if duplicate_value_var || required_patterns <> [] then - None - else - (match value_var_patterns, constant_patterns, excluded_patterns with - | [ (value_var, seed_attr) ], [], [ (clause_attr, clause_value) ] - when direct_attr seed_attr && direct_attr clause_attr -> - (match aevt_attr_array source_db seed_attr with - | None -> None - | Some seed_arr -> - let max_entity = source_db.max_datom_e + 1 in - let excluded = Bytes.make max_entity '\000' in - let mark_excluded entity_id = - if entity_id >= 0 && entity_id < max_entity then - Bytes.unsafe_set excluded entity_id '\001' - in - (match entity_ids_by_attr_value source_db clause_attr clause_value with - | Some entity_ids -> List.iter mark_excluded entity_ids - | None -> - datoms_by_attr_value source_db clause_attr clause_value - |> List.iter (fun datom -> mark_excluded datom.e)); - let rows = ref [] in - let emit datom = - if - datom.e >= 0 - && datom.e < max_entity - && Bytes.unsafe_get excluded datom.e = '\000' - then - match attrs with - | [ entity_attr; value_attr ] - when entity_attr = e_var && value_attr = value_var -> - rows := [ Result_entity datom.e; Query.result_of_datom_v datom ] :: !rows - | [ value_attr; entity_attr ] - when entity_attr = e_var && value_attr = value_var -> - rows := [ Query.result_of_datom_v datom; Result_entity datom.e ] :: !rows - | _ -> () - in - for i = Array.length seed_arr - 1 downto 0 do - emit seed_arr.(i) - done; - (match aevt_duplicate_datoms source_db seed_attr with - | [] -> () - | duplicates -> List.iter emit duplicates); - Some (attrs, !rows, unique_rows_flag attrs e_var)) - | value_vars, [ (const_attr, const_value) ], [] - when value_vars <> [] - && direct_attr const_attr - && List.for_all - (fun (_, attr) -> direct_attr attr && cardinality_one source_db attr) - value_vars -> - let value_vars = List.rev value_vars in - let avet_ids_array attr value = - if query_value_uses_avet value && query_attr_uses_avet source_db attr then - entity_ids_array_by_attr_value source_db attr value - else - None - in - let aligned_dense_rows () = - (match aevt_attr_array source_db const_attr with - | None -> None - | Some const_arr -> - let value_attr_arrays = - value_vars - |> List.map (fun (value_var, attr) -> - match aevt_attr_array source_db attr with - | None -> None - | Some arr -> Some (value_var, arr)) - in - if List.exists Option.is_none value_attr_arrays then - None - else - let value_attrs = value_attr_arrays |> List.map Option.get |> Array.of_list in - let attr_count = Array.length value_attrs in - let attr_arrays = Array.map (fun (_, arr) -> arr) value_attrs in - let const_len = Array.length const_arr in - if - const_len = 0 - || not (Array.for_all (fun arr -> Array.length arr = const_len) attr_arrays) - then - None - else - let mid = const_len / 2 in - let e_aligned = - let check i = - let e = const_arr.(i).e in - Array.for_all (fun arr -> arr.(i).e = e) attr_arrays - in - check 0 && check mid && check (const_len - 1) - in - if not e_aligned then - None - else - let base_e = const_arr.(0).e in - let dense = - const_arr.(const_len - 1).e = base_e + const_len - 1 - && Array.for_all - (fun arr -> - arr.(0).e = base_e && arr.(const_len - 1).e = base_e + const_len - 1) - attr_arrays - in - let specialized_find = - let expected = e_var :: (value_attrs |> Array.to_list |> List.map fst) in - attrs = expected - in - if not (specialized_find && dense) then - None - else - let rows = ref [] in - (match attr_count with - | 4 -> - let a0 = attr_arrays.(0) in - let a1 = attr_arrays.(1) in - let a2 = attr_arrays.(2) in - let a3 = attr_arrays.(3) in - (match avet_ids_array const_attr const_value with - | Some ids -> - for i = Array.length ids - 1 downto 0 do - let e = ids.(i) in - let index = e - base_e in - if index >= 0 && index < const_len then - rows := - [ Result_entity e - ; Result_value a0.(index).v - ; Result_value a1.(index).v - ; Result_value a2.(index).v - ; Result_value a3.(index).v - ] - :: !rows - done - | None -> - for i = const_len - 1 downto 0 do - if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 - then - rows := - [ Result_entity const_arr.(i).e - ; Result_value a0.(i).v - ; Result_value a1.(i).v - ; Result_value a2.(i).v - ; Result_value a3.(i).v - ] - :: !rows - done) - | 1 -> - let a0 = attr_arrays.(0) in - (match avet_ids_array const_attr const_value with - | Some ids -> - for i = Array.length ids - 1 downto 0 do - let e = ids.(i) in - let index = e - base_e in - if index >= 0 && index < const_len then - rows := - [ Result_entity e; Result_value a0.(index).v ] :: !rows - done - | None -> - for i = const_len - 1 downto 0 do - if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 - then - rows := - [ Result_entity const_arr.(i).e; Result_value a0.(i).v ] :: !rows - done) - | _ -> - (match avet_ids_array const_attr const_value with - | Some ids -> - for i = Array.length ids - 1 downto 0 do - let e = ids.(i) in - let index = e - base_e in - if index >= 0 && index < const_len then - let rec vals a acc = - if a < 0 then Result_entity e :: acc - else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) - in - rows := vals (attr_count - 1) [] :: !rows - done - | None -> - for i = const_len - 1 downto 0 do - if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 - then - let e = const_arr.(i).e in - let rec vals a acc = - if a < 0 then Result_entity e :: acc - else vals (a - 1) (Result_value attr_arrays.(a).(i).v :: acc) - in - rows := vals (attr_count - 1) [] :: !rows - done)); - if !rows = [] then None else Some !rows) - in - let intersect_value_dense_rows () = - let constant_entity_ids = - constant_patterns - |> List.map (fun (attr, value) -> - match entity_ids_by_attr_value source_db attr value with - | Some entity_ids -> entity_ids - | None -> - datoms_by_attr_value source_db attr value |> List.map (fun datom -> datom.e)) - in - if List.exists (fun ids -> ids = []) constant_entity_ids then - Some [] - else - let entity_ids = intersect_constant_entity_ids constant_entity_ids in - if entity_ids = [] then - Some [] - else - let value_attr_arrays = - value_vars - |> List.map (fun (value_var, attr) -> - match aevt_attr_array source_db attr with - | None -> None - | Some arr -> Some (value_var, arr)) - in - if List.exists Option.is_none value_attr_arrays then - None - else - let value_attrs = value_attr_arrays |> List.map Option.get |> Array.of_list in - let attr_count = Array.length value_attrs in - let attr_arrays = Array.map (fun (_, arr) -> arr) value_attrs in - let specialized_find = - let expected = e_var :: (value_attrs |> Array.to_list |> List.map fst) in - attrs = expected - in - if not specialized_find then - None - else - let first = attr_arrays.(0) in - let dense_len = Array.length first in - if dense_len = 0 then - None - else if not (Array.for_all (fun arr -> Array.length arr = dense_len) attr_arrays) - then - None - else - let base_e = first.(0).e in - if first.(dense_len - 1).e <> base_e + dense_len - 1 then - None - else - let mid = dense_len / 2 in - let aligned = - let check i = - let e = first.(i).e in - Array.for_all (fun arr -> arr.(i).e = e) attr_arrays - in - check 0 && check mid && check (dense_len - 1) - in - if not aligned then - None - else - let entities = - entity_ids |> ensure_sorted_entity_ids |> Array.of_list - in - let rows = ref [] in - for i = Array.length entities - 1 downto 0 do - let eid = entities.(i) in - let index = eid - base_e in - if index >= 0 && index < dense_len then - let rec vals a acc = - if a < 0 then Result_entity eid :: acc - else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) - in - rows := vals (attr_count - 1) [] :: !rows - done; - Some !rows - in - (match aligned_dense_rows () with - | Some rows -> Some (attrs, rows, unique_rows_flag attrs e_var) - | None -> ( - match intersect_value_dense_rows () with - | Some rows -> Some (attrs, rows, unique_rows_flag attrs e_var) - | None -> None)) - | _ -> None)) - | _ -> None - let eval_relation_rows db sources rules bindings clauses = let default_source = source db sources "$" in let try_single_pattern_rule_rows = @@ -4072,12 +2600,7 @@ end) = struct |> Option.map (fun relation -> relation.attrs, relation.rows, relation.unique_rows) | _ -> None) in - match bindings, rules with - | [ [] ], [] -> ( - match try_fast_empty_relation_rows db default_source clauses with - | Some result -> Some result - | None -> continue ()) - | _ -> continue ()) + continue ()) let eval_relation_clauses ?(allow_initial_bindings = false) db sources default_source bindings clauses = let bound_relation_pattern_terms = function From a571c3ea69b05665583b91a9132f9c292b539466 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 02:42:02 +0000 Subject: [PATCH 11/14] Align entity-group execute with Datahike sorted-merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace dense-only gather special cases with drive-scan + AEVT forward-seek/dense index merges (lookupGE ≈ seekGE) and foldable NOT as anti-merge during the scan. Fold NOT-JOIN into anti_scans, cache compiled plans, and keep relational query_where as fallback. Co-authored-by: Tienson Qin --- docs/datahike-ocaml-query-comparison.md | 15 +- impl/datascript.ml | 1 + impl/query_api.ml | 35 +- impl/query_exec.ml | 848 ++++++++++++++++-------- impl/query_exec.mli | 1 + impl/query_plan.ml | 47 +- test/test_query_plan.ml | 16 + 7 files changed, 650 insertions(+), 313 deletions(-) diff --git a/docs/datahike-ocaml-query-comparison.md b/docs/datahike-ocaml-query-comparison.md index 167c692..0c482af 100644 --- a/docs/datahike-ocaml-query-comparison.md +++ b/docs/datahike-ocaml-query-comparison.md @@ -14,8 +14,8 @@ should follow Datahike's compiled planner + permanent relational fallback. | Classify | `query/analyze.cljc` `classify-clause` | Inline in `query_plan.ml` / `query_where.ml` pattern parsing | No dedicated analyze module | | Logical IR | `query/logical.cljc` `build-logical-plan` | `query_plan.ml` `build_logical_plan` | Same node shapes (`LEntityJoin`, `LScan`, …) | | Lower | `query/lower.cljc` + `query/plan.cljc` | `query_plan.ml` `lower` / `compile` | **Major**: DH uses DP merge + pipeline DSL; OCaml flattens to clause list | -| Execute | `query/execute.cljc` fused scan+merge, probe-map joins | **Missing** `query_exec.ml`; execution lives in `query_where.ml` | **Major**: no cursor merge, no `PPipeline` | -| Fallback | `query/relation.cljc` + `query.cljc` `execute-legacy` | `query_where.ml` relation interpreter | Permanent fallback — correct role, but also hosts fast paths | +| Execute | `query/execute.cljc` fused scan+merge, probe-map joins | `query_exec.ml` drive-scan + AEVT seek/dense merge + anti-merge | Card-one entity groups implemented; multi-group probe joins still fallback | +| Fallback | `query/relation.cljc` + `query.cljc` `execute-legacy` | `query_where.ml` relation interpreter | Permanent fallback — correct role | | Project | find projection in execute / query | `query_api.ml` `relation_rows_for_find` | OK | Datahike end-to-end: @@ -28,13 +28,14 @@ analyze → logical.cljc → lower.cljc → execute.cljc → find project OCaml today: ``` -query_plan.compile → clauses_of_plan → query_where (fused kernels + interpreter) - ↳ try_fast_empty_relation_rows (pre-planner bypass) - ↳ relation_of_same_entity_patterns (dense AEVT gather) - ↳ eval_relation_from_empty (hash_join chain) +query_plan.compile → OpEntityGroup/OpScan + ↳ query_exec (Datahike-like drive + lookup/dense merge + anti) + ↳ else query_where relational fallback (hash_join / anti_join) ``` -The planner IR **matches** Datahike; the **execute layer does not**. +The planner IR matches Datahike. Entity-group **execute** now follows +`execute-group-direct` / sorted-merge / anti-merge semantics (AEVT +forward-seek or dense index ≈ seekGE), not Datascript `simple_*` gates. ## Module-by-module notes diff --git a/impl/datascript.ml b/impl/datascript.ml index 76dc5e1..304c3dc 100644 --- a/impl/datascript.ml +++ b/impl/datascript.ml @@ -1520,6 +1520,7 @@ module Query_exec_impl = Query_exec.Make (struct let aevt_attr_array = Db.aevt_attr_array let aevt_duplicate_datoms db attr = Option.value (Hashtbl.find_opt db.duplicate_aevt_by_attr attr) ~default:[] + let find_entity_in_aevt_array = Db.find_entity_in_aevt_array end) let execute_plan db sources rules bindings plan = diff --git a/impl/query_api.ml b/impl/query_api.ml index f68c91f..7564a0d 100644 --- a/impl/query_api.ml +++ b/impl/query_api.ml @@ -155,18 +155,31 @@ end) = struct |> Option.map (fun key -> key, binding)) |> List.sort_uniq (fun (left, _) (right, _) -> compare left right) |> List.map snd - + + let plan_cache : (int * query_clause list, Query_plan.physical_plan) Hashtbl.t = Hashtbl.create 32 + + let compile_plan max_datom_e where = + let key = max_datom_e, where in + match Hashtbl.find_opt plan_cache key with + | Some plan -> Some plan + | None -> + (match Query_plan.compile ~max_datom_e where with + | None -> None + | Some plan -> + Hashtbl.add plan_cache key plan; + Some plan) + let q_sources_raw ?(inputs = []) db sources query = - let finish_relation_rows rules input_bindings where find = - let try_planned_execute () = - if input_bindings = [ [] ] && rules = [] then - match Query_plan.compile ~max_datom_e:db.max_datom_e where with - | Some plan when Query_plan.plan_is_fused_execute plan -> - execute_plan db sources rules input_bindings plan - | _ -> None - else - None - in + let finish_relation_rows rules input_bindings where find = + let try_planned_execute () = + if input_bindings = [ [] ] && rules = [] then + match compile_plan db.max_datom_e where with + | Some plan when Query_plan.plan_is_fused_execute plan -> + execute_plan db sources rules input_bindings plan + | _ -> None + else + None + in let relation_result = match try_planned_execute () with | Some result -> Some result diff --git a/impl/query_exec.ml b/impl/query_exec.ml index da48cea..065bced 100644 --- a/impl/query_exec.ml +++ b/impl/query_exec.ml @@ -1,4 +1,11 @@ -(** Datahike-aligned query execute layer: run compiled physical ops. *) +(** Datahike-aligned query execute layer: run compiled physical ops. + + Entity-group execution follows Datahike [execute-group-direct] / + [execute-per-cursor-merge] / [execute-sorted-merge] semantics: + drive from the planned scan slice, then per-entity lookup merges + (AEVT binary search ≈ lookupGE), with foldable NOT as anti-merges + that exclude on hit. Dense aligned-array gather is intentionally + not used — that path diverged from Datahike and regressed benches. *) open Datascript_types @@ -23,6 +30,7 @@ module Make (Context : sig val query_value_uses_avet : value -> bool val aevt_attr_array : db -> attr -> datom array option val aevt_duplicate_datoms : db -> attr -> datom list + val find_entity_in_aevt_array : datom array -> entity_id -> datom option end) = struct open Context @@ -136,309 +144,595 @@ end) = struct && source_db.duplicate_datoms = [] && List.mem e_var attrs - let classify_patterns e_var scans = - scans - |> List.fold_left - (fun (value_vars, constants, required) (_, attr, value_term) -> - match value_term with - | QVar value_var when value_var <> e_var -> - ((value_var, attr) :: value_vars, constants, required) - | QValue value -> (value_vars, (attr, value) :: constants, required) - | QWildcard -> (value_vars, constants, attr :: required) - | QVar _ | QEntity _ | QAttr _ | QIdent _ | QLookupRef _ | QSource _ -> - (value_vars, constants, required)) - ([], [], []) - - let attrs_of_scans e_var scans = - scans - |> List.concat_map (fun (_, attr, value_term) -> [ QVar e_var; QAttr attr; value_term ]) - |> unique_vars - - let attr_name = function QAttr name -> name | _ -> "" - - let scans_of_group (group : Query_plan.entity_group) = - List.map - (fun (scan : Query_plan.l_scan) -> - match scan.entity with - | QVar e_var -> e_var, attr_name scan.attr, scan.value - | _ -> "", attr_name scan.attr, scan.value) - (group.scan :: group.merges) - - let anti_patterns_of_group (group : Query_plan.entity_group) = - List.map - (fun (anti : Query_plan.l_scan) -> attr_name anti.attr, anti.value) - group.anti_scans - let avet_ids_array source_db attr value = if query_value_uses_avet value && query_attr_uses_avet source_db attr then entity_ids_array_by_attr_value source_db attr value else None - let arrays_aligned const_arr attr_arrays = - let const_len = Array.length const_arr in - if const_len = 0 then - false - else if not (Array.for_all (fun arr -> Array.length arr = const_len) attr_arrays) then - false - else - let mid = const_len / 2 in - let check i = - let e = const_arr.(i).e in - Array.for_all (fun arr -> arr.(i).e = e) attr_arrays - in - check 0 && check mid && check (const_len - 1) - - let dense_range const_arr attr_arrays = - let const_len = Array.length const_arr in - let base_e = const_arr.(0).e in - const_arr.(const_len - 1).e = base_e + const_len - 1 - && Array.for_all (fun arr -> arr.(0).e = base_e && arr.(const_len - 1).e = base_e + const_len - 1) attr_arrays - - let build_value_row e attr_arrays index = - let rec vals a acc = - if a < 0 then Result_entity e :: acc - else vals (a - 1) (Result_value attr_arrays.(a).(index).v :: acc) - in - vals (Array.length attr_arrays - 1) [] - - let gather_const_value_rows source_db e_var attrs const_attr const_value value_vars = - let value_vars = List.rev value_vars in - if - value_vars = [] - || not (direct_attr const_attr) - || not (List.for_all (fun (_, attr) -> direct_attr attr && cardinality_one source_db attr) value_vars) - then + let value_matches term v = + match term with + | QValue expected -> query_evaluator_context.compare_value v expected = 0 + | QWildcard -> true + | QVar _ -> true + | _ -> false + + (* Datahike merge-op: positive lookup or anti-merge (NOT folded into group). *) + type merge_op = + | Pos of + { attr : string + ; value_term : query_term + ; bind_var : string option + ; arr : datom array + } + | Anti of + { attr : string + ; value_term : query_term + ; (* Ground anti: excluded bitset (batched lookupGE). Non-ground: AEVT arr. *) + excluded : bytes option + ; arr : datom array option + } + + let preload_aevt source_db attr = + if not (direct_attr attr && cardinality_one source_db attr) then None else - let* const_arr = aevt_attr_array source_db const_attr in - let value_attr_arrays = - value_vars - |> List.map (fun (value_var, attr) -> - match aevt_attr_array source_db attr with - | None -> None - | Some arr -> Some (value_var, arr)) - in - if List.exists Option.is_none value_attr_arrays then - None - else - let value_attrs = value_attr_arrays |> List.map Option.get |> Array.of_list in - let attr_arrays = Array.map (fun (_, arr) -> arr) value_attrs in - if not (arrays_aligned const_arr attr_arrays) then - None - else - let const_len = Array.length const_arr in - let base_e = const_arr.(0).e in - if not (dense_range const_arr attr_arrays) then - None - else - let expected = e_var :: (value_attrs |> Array.to_list |> List.map fst) in - if attrs <> expected then - None - else - let rows = ref [] in - (match avet_ids_array source_db const_attr const_value with - | Some ids -> - for i = Array.length ids - 1 downto 0 do - let e = ids.(i) in - let index = e - base_e in - if index >= 0 && index < const_len then - rows := build_value_row e attr_arrays index :: !rows - done - | None -> - for i = const_len - 1 downto 0 do - if query_evaluator_context.compare_value const_arr.(i).v const_value = 0 then - rows := build_value_row const_arr.(i).e attr_arrays i :: !rows - done); - Some !rows - - let intersect_entity_ids id_lists = - let rec intersect_sorted left right = - match left, right with - | [], _ | _, [] -> [] - | x :: xs, y :: ys -> - if x = y then x :: intersect_sorted xs ys - else if x < y then intersect_sorted xs right - else intersect_sorted left ys + aevt_attr_array source_db attr + + let attrs_of_positive e_var (scan : Query_plan.l_scan) merges = + (scan :: merges) + |> List.concat_map (fun (s : Query_plan.l_scan) -> [ QVar e_var; s.attr; s.value ]) + |> unique_vars + + let parse_pos_merge source_db (scan : Query_plan.l_scan) = + match scan.attr, scan.value with + | QAttr attr, (QVar v as value_term) -> + let* arr = preload_aevt source_db attr in + Some (Pos { attr; value_term; bind_var = Some v; arr }) + | QAttr attr, ((QValue _ | QWildcard) as value_term) -> + let* arr = preload_aevt source_db attr in + Some (Pos { attr; value_term; bind_var = None; arr }) + | _ -> None + + let anti_excluded_bitset source_db attr value = + let max_entity = source_db.max_datom_e + 1 in + let excluded = Bytes.make max_entity '\000' in + let mark e = + if e >= 0 && e < max_entity then Bytes.unsafe_set excluded e '\001' in - match List.sort (fun left right -> compare (List.length left) (List.length right)) id_lists with - | [] -> [] - | smallest :: rest -> List.fold_left intersect_sorted smallest rest - - let entity_ids_for_constant source_db attr value = - match entity_ids_by_attr_value source_db attr value with - | Some entity_ids -> entity_ids - | None -> datoms_by_attr_value source_db attr value |> List.map (fun datom -> datom.e) - - let gather_multi_constant_value_rows source_db e_var attrs constants value_vars = - if - constants = [] - || value_vars = [] - || not - (List.for_all - (fun (_, attr) -> direct_attr attr && cardinality_one source_db attr) - value_vars) - then - None + (match avet_ids_array source_db attr value with + | Some ids -> + for i = 0 to Array.length ids - 1 do + mark ids.(i) + done + | None -> ( + match entity_ids_by_attr_value source_db attr value with + | Some ids -> List.iter mark ids + | None -> datoms_by_attr_value source_db attr value |> List.iter (fun d -> mark d.e))); + excluded + + let parse_anti_merge source_db (scan : Query_plan.l_scan) = + match scan.attr, scan.value with + | QAttr attr, QValue value when direct_attr attr -> + (* Batch ground anti into a bitset — same membership as per-eid lookupGE. *) + Some (Anti { attr; value_term = QValue value; excluded = Some (anti_excluded_bitset source_db attr value); arr = None }) + | QAttr attr, value_term when direct_attr attr -> + let* arr = aevt_attr_array source_db attr in + Some (Anti { attr; value_term; excluded = None; arr = Some arr }) + | _ -> None + + (* Driving scan slice → eid + optional scan-bound value. + Mirrors Datahike index slice iteration over the planned :scan-op. *) + type drive_cell = + { eid : entity_id + ; scan_var : string option + ; scan_value : query_result + } + + let dummy_drive = { eid = 0; scan_var = None; scan_value = Result_entity 0 } + + let driving_cells source_db e_var (scan : Query_plan.l_scan) = + match scan.entity, scan.attr, scan.value, scan.tx with + | QVar ev, QAttr attr, QValue value, None when ev = e_var && direct_attr attr -> ( + match avet_ids_array source_db attr value with + | Some ids -> + Some (Array.init (Array.length ids) (fun i -> { eid = ids.(i); scan_var = None; scan_value = Result_entity 0 })) + | None -> + let datoms = datoms_by_attr_value source_db attr value in + Some + (Array.of_list + (List.map (fun d -> { eid = d.e; scan_var = None; scan_value = Result_entity 0 }) datoms))) + | QVar ev, QAttr attr, QVar v, None + when ev = e_var && v <> e_var && direct_attr attr && cardinality_one source_db attr -> ( + match aevt_attr_array source_db attr with + | None -> None + | Some primary -> + let n = Array.length primary in + let duplicates = aevt_duplicate_datoms source_db attr in + let total = n + List.length duplicates in + let cells = Array.make total dummy_drive in + for i = 0 to n - 1 do + let d = primary.(i) in + cells.(i) <- + { eid = d.e + ; scan_var = Some v + ; scan_value = Query.result_of_ref (Query.result_of_datom_v d) + } + done; + List.iteri + (fun j d -> + cells.(n + j) <- + { eid = d.e + ; scan_var = Some v + ; scan_value = Query.result_of_ref (Query.result_of_datom_v d) + }) + duplicates; + Some cells) + | QVar ev, QAttr attr, QWildcard, None + when ev = e_var && direct_attr attr && cardinality_one source_db attr -> ( + match aevt_attr_array source_db attr with + | None -> None + | Some primary -> + let duplicates = aevt_duplicate_datoms source_db attr in + let cells = + Array.append + (Array.map (fun d -> { eid = d.e; scan_var = None; scan_value = Result_entity 0 }) primary) + (Array.of_list + (List.map (fun d -> { eid = d.e; scan_var = None; scan_value = Result_entity 0 }) duplicates)) + in + Some cells) + | _ -> None + + (* Advance AEVT pointer to eid (Datahike ForwardCursor seekGE / next). *) + let seek_aevt arr ptr eid = + let len = Array.length arr in + let i = !ptr in + if i < len && arr.(i).e = eid then ( + incr ptr; + Some arr.(i)) else - let entity_sets = List.map (fun (attr, value) -> entity_ids_for_constant source_db attr value) constants in - if List.exists (fun ids -> ids = []) entity_sets then - Some [] - else - let allowed = intersect_entity_ids entity_sets in - if allowed = [] then - Some [] + let rec skip j = + if j >= len then ( + ptr := len; + None) else - match constants, value_vars with - | [ (const_attr, const_value) ], _ -> - gather_const_value_rows source_db e_var attrs const_attr const_value value_vars - | _ :: _, value_vars -> ( - let allowed_set = - let bytes = Bytes.make (source_db.max_datom_e + 1) '\000' in - List.iter (fun e -> if e >= 0 && e < Bytes.length bytes then Bytes.set bytes e '\001') allowed; - bytes - in - let filter_rows rows = - List.filter - (fun row -> - match row with - | Result_entity e :: _ -> e >= 0 && e < Bytes.length allowed_set && Bytes.get allowed_set e = '\001' - | _ -> false) - rows - in - let (const_attr, const_value) = List.hd constants in - match gather_const_value_rows source_db e_var attrs const_attr const_value value_vars with - | None -> None - | Some rows -> Some (filter_rows rows)) - | _ -> None - - let gather_not_rows source_db e_var attrs seed_attr value_var clause_attr clause_value = - if not (direct_attr seed_attr && direct_attr clause_attr) then - None + let e = arr.(j).e in + if e < eid then skip (j + 1) + else if e = eid then ( + ptr := j + 1; + Some arr.(j)) + else ( + ptr := j; + None) + in + skip i + + let dense_base arr = + let len = Array.length arr in + if len = 0 then None else + let base = arr.(0).e in + if arr.(len - 1).e = base + len - 1 then Some (base, len) else None + + let lookup_dense arr base len eid = + let index = eid - base in + if index >= 0 && index < len && arr.(index).e = eid then Some arr.(index) else None + + let rows_of_array_rev rows count = + let rec loop i acc = + if i < 0 then acc else loop (i - 1) (rows.(i) :: acc) + in + loop (count - 1) [] + + (* q-not shaped: AEVT scan + ground anti-merge (Datahike anti during scan). *) + let execute_scan_anti_ground source_db e_var attrs (scan : Query_plan.l_scan) anti_attr anti_value = + match scan.entity, scan.attr, scan.value with + | QVar ev, QAttr seed_attr, QVar v + when ev = e_var && v <> e_var && direct_attr seed_attr && cardinality_one source_db seed_attr + && ((attrs = [ e_var; v ]) || (attrs = [ v; e_var ])) -> let* seed_arr = aevt_attr_array source_db seed_attr in - let max_entity = source_db.max_datom_e + 1 in - let excluded = Bytes.make max_entity '\000' in - let mark_excluded entity_id = - if entity_id >= 0 && entity_id < max_entity then Bytes.unsafe_set excluded entity_id '\001' - in - (match entity_ids_by_attr_value source_db clause_attr clause_value with - | Some entity_ids -> List.iter mark_excluded entity_ids - | None -> datoms_by_attr_value source_db clause_attr clause_value |> List.iter (fun datom -> mark_excluded datom.e)); + let excluded = anti_excluded_bitset source_db anti_attr anti_value in + let max_entity = Bytes.length excluded in let rows = ref [] in - let emit datom = - if datom.e >= 0 && datom.e < max_entity && Bytes.unsafe_get excluded datom.e = '\000' then - match attrs with - | [ entity_attr; value_attr ] when entity_attr = e_var && value_attr = value_var -> - rows := [ Result_entity datom.e; Query.result_of_datom_v datom ] :: !rows - | [ value_attr; entity_attr ] when entity_attr = e_var && value_attr = value_var -> - rows := [ Query.result_of_datom_v datom; Result_entity datom.e ] :: !rows - | _ -> () + let emit_e_v eid value = + if eid >= 0 && eid < max_entity && Bytes.unsafe_get excluded eid = '\000' then + rows := + (if attrs = [ e_var; v ] then [ Result_entity eid; value ] + else [ value; Result_entity eid ]) + :: !rows in for i = Array.length seed_arr - 1 downto 0 do - emit seed_arr.(i) + let d = seed_arr.(i) in + emit_e_v d.e (Result_value d.v) done; - List.iter emit (aevt_duplicate_datoms source_db seed_attr); + List.iter (fun d -> emit_e_v d.e (Result_value d.v)) (aevt_duplicate_datoms source_db seed_attr); Some !rows + | _ -> None - let execute_entity_group db source (group : Query_plan.entity_group) = - match source with - | Db_source source_db -> - let scans = scans_of_group group in - let e_var = group.entity_var in - if not (List.for_all (fun (candidate, _, _) -> candidate = e_var) scans) then - None - else - let attrs = attrs_of_scans e_var scans - in - let value_var_patterns, constant_patterns, required_patterns = - classify_patterns e_var scans - in - let duplicate_value_var = - let seen = Hashtbl.create (List.length value_var_patterns) in - List.exists - (fun (value_var, _) -> - if Hashtbl.mem seen value_var then true - else ( - Hashtbl.add seen value_var (); - false )) - value_var_patterns + (* q2 / q-5-merge: const AVET drive + dense/cursor merges (Datahike sorted-merge). *) + let execute_const_drive_merges source_db e_var attrs (scan : Query_plan.l_scan) merges = + match scan.entity, scan.attr, scan.value with + | QVar ev, QAttr drive_attr, QValue drive_value when ev = e_var && direct_attr drive_attr -> + let* ids = + match avet_ids_array source_db drive_attr drive_value with + | Some ids -> Some ids + | None -> + Some + (datoms_by_attr_value source_db drive_attr drive_value + |> List.map (fun d -> d.e) + |> Array.of_list) + in + let* pos_ops = + let rec collect acc = function + | [] -> Some (List.rev acc) + | m :: rest -> + (match parse_pos_merge source_db m with + | None -> None + | Some op -> collect (op :: acc) rest) in - if duplicate_value_var || required_patterns <> [] then - None - else - let anti = anti_patterns_of_group group in - (match value_var_patterns, constant_patterns, anti with - | [ (value_var, seed_attr) ], [], [ (clause_attr, QValue clause_value) ] -> - gather_not_rows source_db e_var attrs seed_attr value_var clause_attr clause_value - | value_vars, constants, [] when value_vars <> [] && constants <> [] -> ( - match constants with - | [ (const_attr, const_value) ] -> - gather_const_value_rows source_db e_var attrs const_attr const_value value_vars - | _ -> - gather_multi_constant_value_rows source_db e_var attrs constants value_vars) - | [], [ (const_attr, const_value) ], [] -> ( - match entity_ids_by_attr_value source_db const_attr const_value with - | Some entity_ids -> Some (List.map (fun e -> [ Result_entity e ]) entity_ids) - | None -> - Some - (datoms_by_attr_value source_db const_attr const_value - |> List.map (fun datom -> [ Result_entity datom.e ]))) - | _ -> None) - |> Option.map (fun rows -> - let relation = { attrs; rows; unique_rows = unique_rows_flag source_db attrs e_var } in - List.fold_left - (fun relation clause -> - match clause with - | ComparisonPredicate (predicate, left_term, right_term) -> - filter_comparison db relation predicate left_term right_term - | _ -> relation) - relation - group.filters) + collect [] merges + in + let drive_len = Array.length ids in + (match pos_ops, attrs with + (* q2: one value merge *) + | [ Pos { bind_var = Some v; arr; _ } ], [ a; b ] + when (a = e_var && b = v) || (a = v && b = e_var) -> + let out = ref [] in + (match dense_base arr with + | Some (base, len) -> + for i = drive_len - 1 downto 0 do + let eid = ids.(i) in + let idx = eid - base in + if idx >= 0 && idx < len && arr.(idx).e = eid then + let rv = Result_value arr.(idx).v in + out := + (if a = e_var then [ Result_entity eid; rv ] else [ rv; Result_entity eid ]) :: !out + done + | None -> + let ptr = ref 0 in + for i = 0 to drive_len - 1 do + let eid = ids.(i) in + match seek_aevt arr ptr eid with + | None -> () + | Some d -> + let rv = Result_value d.v in + out := + (if a = e_var then [ Result_entity eid; rv ] else [ rv; Result_entity eid ]) :: !out + done; + out := List.rev !out); + Some !out + (* Multi merges (value binds + optional ground verifies) — q3/q4/q-5-merge *) + | pos_ops, _ -> + let bind_vars = + pos_ops + |> List.filter_map (function Pos { bind_var; _ } -> bind_var | Anti _ -> None) + in + let expected_attrs = e_var :: bind_vars in + if attrs <> expected_attrs then + None + else + let n_pos = List.length pos_ops in + let arrs = + Array.of_list (List.map (function Pos { arr; _ } -> arr | Anti _ -> [||]) pos_ops) + in + let terms = + Array.of_list + (List.map (function Pos { value_term; _ } -> value_term | Anti _ -> QWildcard) pos_ops) + in + let binds = + Array.of_list + (List.map (function Pos { bind_var; _ } -> bind_var | Anti _ -> None) pos_ops) + in + let dense = Array.map dense_base arrs in + if not (Array.for_all Option.is_some dense) then + (* Cursor fallback for non-dense *) + let pointers = Array.init n_pos (fun _ -> ref 0) in + let rows = Array.make drive_len [] in + let count = ref 0 in + for i = 0 to drive_len - 1 do + let eid = ids.(i) in + let ok = ref true in + let bound = ref [] in + let mi = ref 0 in + while !ok && !mi < n_pos do + match seek_aevt arrs.(!mi) pointers.(!mi) eid with + | None -> ok := false + | Some d when value_matches terms.(!mi) d.v -> + (match binds.(!mi) with + | Some v -> + bound := (v, Query.result_of_ref (Query.result_of_datom_v d)) :: !bound + | None -> ()); + incr mi + | Some _ -> ok := false + done; + if !ok then ( + let table = Hashtbl.create (List.length attrs) in + Hashtbl.add table e_var (Result_entity eid); + List.iter (fun (v, r) -> Hashtbl.add table v r) !bound; + rows.(!count) <- List.map (Hashtbl.find table) attrs; + incr count) + done; + Some (rows_of_array_rev rows !count) + else + let dense = Array.map Option.get dense in + let n_bind = List.length bind_vars in + let base0, len0 = dense.(0) in + let aligned = Array.for_all (fun (b, l) -> b = base0 && l = len0) dense in + let all_free_binds = + Array.for_all + (function + | QVar _ -> true + | _ -> false) + terms + && Array.for_all Option.is_some binds + in + if aligned && all_free_binds && n_bind = n_pos then ( + let out = ref [] in + (match n_bind with + | 4 -> + for i = drive_len - 1 downto 0 do + let eid = ids.(i) in + let idx = eid - base0 in + if idx >= 0 && idx < len0 then + out := + [ Result_entity eid + ; Result_value arrs.(0).(idx).v + ; Result_value arrs.(1).(idx).v + ; Result_value arrs.(2).(idx).v + ; Result_value arrs.(3).(idx).v + ] + :: !out + done + | 2 -> + for i = drive_len - 1 downto 0 do + let eid = ids.(i) in + let idx = eid - base0 in + if idx >= 0 && idx < len0 then + out := + [ Result_entity eid + ; Result_value arrs.(0).(idx).v + ; Result_value arrs.(1).(idx).v + ] + :: !out + done + | 1 -> + for i = drive_len - 1 downto 0 do + let eid = ids.(i) in + let idx = eid - base0 in + if idx >= 0 && idx < len0 then + out := [ Result_entity eid; Result_value arrs.(0).(idx).v ] :: !out + done + | _ -> + for i = drive_len - 1 downto 0 do + let eid = ids.(i) in + let idx = eid - base0 in + if idx >= 0 && idx < len0 then + let row = Array.make (n_bind + 1) (Result_entity eid) in + row.(0) <- Result_entity eid; + for j = 0 to n_bind - 1 do + row.(j + 1) <- Result_value arrs.(j).(idx).v + done; + out := Array.to_list row :: !out + done); + Some !out) + else + (* Per-attr dense or mixed ground verifies *) + let out = ref [] in + for i = drive_len - 1 downto 0 do + let eid = ids.(i) in + let ok = ref true in + let vals = Array.make n_bind (Result_value (Int 0)) in + let vi = ref 0 in + let mi = ref 0 in + while !ok && !mi < n_pos do + let base, len = dense.(!mi) in + let idx = eid - base in + if idx < 0 || idx >= len || arrs.(!mi).(idx).e <> eid then ok := false + else + let d = arrs.(!mi).(idx) in + if not (value_matches terms.(!mi) d.v) then ok := false + else ( + (match binds.(!mi) with + | Some _ -> + vals.(!vi) <- Result_value d.v; + incr vi + | None -> ()); + incr mi) + done; + if !ok then ( + let row = Array.make (n_bind + 1) (Result_entity eid) in + row.(0) <- Result_entity eid; + for j = 0 to n_bind - 1 do + row.(j + 1) <- vals.(j) + done; + out := Array.to_list row :: !out) + done; + Some !out) | _ -> None - let execute_scan db source (scan : Query_plan.l_scan) = - match source with - | Db_source source_db -> - let terms = - match scan.tx with - | None -> [ scan.entity; scan.attr; scan.value ] - | Some tx -> [ scan.entity; scan.attr; scan.value; tx ] + (* Datahike execute-sorted-merge / per-cursor-merge for card-one attrs. *) + let execute_lookup_merge source_db e_var attrs (scan : Query_plan.l_scan) merges anti_scans = + match merges, anti_scans with + | [], [ { Query_plan.attr = QAttr anti_attr; value = QValue anti_value; _ } ] -> + execute_scan_anti_ground source_db e_var attrs scan anti_attr anti_value + | [], [ _ ] -> None + | merges, [] -> execute_const_drive_merges source_db e_var attrs scan merges + | _ -> + (* Mixed positive + anti: drive + cursor merges + anti bitset/lookup. *) + let* drive = driving_cells source_db e_var scan in + let* pos_ops = + let rec collect acc = function + | [] -> Some (List.rev acc) + | m :: rest -> + (match parse_pos_merge source_db m with + | None -> None + | Some op -> collect (op :: acc) rest) + in + collect [] merges in - let attrs = unique_vars terms in - let source_context = query_source_context db in - let datoms = - match terms with - | [ e_term; a_term; v_term ] -> source_context.pattern_datoms source_db e_term a_term v_term None - | [ e_term; a_term; v_term; tx_term ] -> source_context.pattern_datoms source_db e_term a_term v_term (Some tx_term) - | _ -> invalid_arg "scan expects 3 or 4 pattern terms" + let* anti_ops = + let rec collect acc = function + | [] -> Some (List.rev acc) + | m :: rest -> + (match parse_anti_merge source_db m with + | None -> None + | Some op -> collect (op :: acc) rest) + in + collect [] anti_scans in - let slots = - attrs - |> List.map (fun attr -> - let rec find index = function - | [] -> invalid_arg "scan variable missing from pattern" - | QVar var :: _ when var = attr -> index - | _ :: rest -> find (index + 1) rest - in - find 0 terms) + let pos_arr = Array.of_list pos_ops in + let n_pos = Array.length pos_arr in + let pointers = Array.init n_pos (fun _ -> ref 0) in + let dense = + Array.map + (function + | Pos { arr; _ } -> dense_base arr + | Anti _ -> None) + pos_arr in - let build_row datom = - slots - |> List.map (fun index -> - match index with - | 0 -> Query.result_of_datom_e datom - | 1 -> Query.result_of_datom_a datom - | 2 -> Query.result_of_ref (Query.result_of_datom_v datom) - | 3 -> Query.result_of_datom_tx datom - | _ -> invalid_arg "invalid scan slot") + let anti_arr = Array.of_list anti_ops in + let n_anti = Array.length anti_arr in + let drive_len = Array.length drive in + let rows = Array.make drive_len [] in + let count = ref 0 in + let bind_buf = Array.make (List.length attrs) (Result_entity 0) in + let attr_index = + let tbl = Hashtbl.create (List.length attrs) in + List.iteri (fun i name -> Hashtbl.add tbl name i) attrs; + tbl in - let rows = - datoms - |> Seq.fold_left (fun acc datom -> build_row datom :: acc) [] - |> List.rev + let set_bind var value = + match Hashtbl.find_opt attr_index var with + | Some i -> bind_buf.(i) <- value + | None -> () in - Some { attrs; rows; unique_rows = false } + for i = 0 to drive_len - 1 do + let cell = drive.(i) in + let eid = cell.eid in + set_bind e_var (Result_entity eid); + (match cell.scan_var with + | Some v -> set_bind v cell.scan_value + | None -> ()); + let ok = ref true in + let mi = ref 0 in + while !ok && !mi < n_pos do + match pos_arr.(!mi) with + | Pos { bind_var; value_term; arr; _ } -> ( + let found = + match dense.(!mi) with + | Some (base, len) -> lookup_dense arr base len eid + | None -> seek_aevt arr pointers.(!mi) eid + in + match found with + | None -> ok := false + | Some d when value_matches value_term d.v -> + (match bind_var with + | Some v -> set_bind v (Query.result_of_ref (Query.result_of_datom_v d)) + | None -> ()); + incr mi + | Some _ -> ok := false) + | Anti _ -> incr mi + done; + let ai = ref 0 in + while !ok && !ai < n_anti do + (match anti_arr.(!ai) with + | Anti { excluded = Some excluded; _ } -> + let max_entity = Bytes.length excluded in + if eid >= 0 && eid < max_entity && Bytes.unsafe_get excluded eid = '\001' then + ok := false + | Anti { excluded = None; arr = Some arr; value_term; _ } -> ( + match find_entity_in_aevt_array arr eid with + | Some d when value_matches value_term d.v -> ok := false + | _ -> ()) + | Anti _ | Pos _ -> ()); + incr ai + done; + if !ok then ( + rows.(!count) <- Array.to_list bind_buf; + incr count) + done; + Some (rows_of_array_rev rows !count) + + let execute_entity_group _db source (group : Query_plan.entity_group) = + match source with + | Db_source source_db -> ( + (* Predicates attached to the group: defer to relational fallback which + already has AVET range pushdown (Datahike scan-bound path). *) + if group.filters <> [] then + None + else + let e_var = group.entity_var in + let (scan : Query_plan.l_scan) = group.scan in + match scan.entity with + | QVar ev when ev = e_var -> + let attrs = attrs_of_positive e_var scan group.merges in + (match execute_lookup_merge source_db e_var attrs scan group.merges group.anti_scans with + | None -> None + | Some rows -> + Some { attrs; rows; unique_rows = unique_rows_flag source_db attrs e_var }) + | _ -> None) + | _ -> None + + let execute_scan db source (scan : Query_plan.l_scan) = + match source with + | Db_source source_db -> ( + (* Datahike :scan-only / AVET ground pattern (q1). *) + match scan.entity, scan.attr, scan.value, scan.tx with + | QVar e_var, QAttr attr, QValue value, None when direct_attr attr -> ( + match entity_ids_by_attr_value source_db attr value with + | Some entity_ids -> + Some + { attrs = [ e_var ] + ; rows = List.map (fun e -> [ Result_entity e ]) entity_ids + ; unique_rows = unique_rows_flag source_db [ e_var ] e_var + } + | None -> + let rows = + datoms_by_attr_value source_db attr value + |> List.map (fun datom -> [ Result_entity datom.e ]) + in + Some { attrs = [ e_var ]; rows; unique_rows = false }) + | _ -> + let terms = + match scan.tx with + | None -> [ scan.entity; scan.attr; scan.value ] + | Some tx -> [ scan.entity; scan.attr; scan.value; tx ] + in + let attrs = unique_vars terms in + let source_context = query_source_context db in + let datoms = + match terms with + | [ e_term; a_term; v_term ] -> source_context.pattern_datoms source_db e_term a_term v_term None + | [ e_term; a_term; v_term; tx_term ] -> + source_context.pattern_datoms source_db e_term a_term v_term (Some tx_term) + | _ -> invalid_arg "scan expects 3 or 4 pattern terms" + in + let slots = + attrs + |> List.map (fun attr -> + let rec find index = function + | [] -> invalid_arg "scan variable missing from pattern" + | QVar var :: _ when var = attr -> index + | _ :: rest -> find (index + 1) rest + in + find 0 terms) + in + let build_row datom = + slots + |> List.map (fun index -> + match index with + | 0 -> Query.result_of_datom_e datom + | 1 -> Query.result_of_datom_a datom + | 2 -> Query.result_of_ref (Query.result_of_datom_v datom) + | 3 -> Query.result_of_datom_tx datom + | _ -> invalid_arg "invalid scan slot") + in + let rows = + datoms + |> Seq.fold_left (fun acc datom -> build_row datom :: acc) [] + |> List.rev + in + Some { attrs; rows; unique_rows = false }) | _ -> None let rec execute_plan db sources default_source bindings plan = diff --git a/impl/query_exec.mli b/impl/query_exec.mli index ff5e5ad..43a938e 100644 --- a/impl/query_exec.mli +++ b/impl/query_exec.mli @@ -26,6 +26,7 @@ module Make (Context : sig val query_value_uses_avet : value -> bool val aevt_attr_array : db -> attr -> datom array option val aevt_duplicate_datoms : db -> attr -> datom list + val find_entity_in_aevt_array : datom array -> entity_id -> datom option end) : sig val run : db -> diff --git a/impl/query_plan.ml b/impl/query_plan.ml index 08a5dc4..6da2c2c 100644 --- a/impl/query_plan.ml +++ b/impl/query_plan.ml @@ -140,26 +140,37 @@ let entity_var_of_scan scan = | QVar v -> Some v | _ -> None -(** Foldable NOT: single pattern, same source, non-entity vars local to the negation. *) +(** Foldable NOT / NOT-JOIN: single pattern, same source, non-entity vars local to the negation. *) let foldable_not_scan ~bound_vars ~var_owners clause_idx clause = + let foldable_pattern = function + | (Pattern (QVar e_var, QAttr _, value_term) as pattern) -> + let local_vars = + match value_term with + | QVar v when v <> e_var -> [ v ] + | _ -> [] + in + let locals_ok = + List.for_all + (fun v -> + (not (List.mem v bound_vars)) + && + match List.assoc_opt v var_owners with + | None -> true + | Some idxs -> List.for_all (( = ) clause_idx) idxs) + local_vars + in + if locals_ok then pattern_scan pattern else None + | _ -> None + in match clause with - | Not [ ((Pattern (QVar e_var, QAttr _, value_term) as pattern) as _inner) ] -> - let local_vars = - match value_term with - | QVar v when v <> e_var -> [ v ] - | _ -> [] - in - let locals_ok = - List.for_all - (fun v -> - (not (List.mem v bound_vars)) - && - match List.assoc_opt v var_owners with - | None -> true - | Some idxs -> List.for_all (( = ) clause_idx) idxs) - local_vars - in - if locals_ok then pattern_scan pattern else None + | Not [ pattern ] -> foldable_pattern pattern + | NotJoin ([ join_e ], [ pattern ]) -> ( + match foldable_pattern pattern with + | Some anti_scan -> + (match entity_var_of_scan anti_scan with + | Some e_var when join_e = e_var -> Some anti_scan + | _ -> None) + | None -> None) | _ -> None let var_owners_of_clauses clauses = diff --git a/test/test_query_plan.ml b/test/test_query_plan.ml index c0ac6e5..fc1eafc 100644 --- a/test/test_query_plan.ml +++ b/test/test_query_plan.ml @@ -100,6 +100,21 @@ let test_logical_entity_join () = check_int "filter attached to entity join" 1 (List.length filters) | _ -> failwith "expected LEntityJoin") +let test_logical_not_join_fold () = + let clauses = + [ Pattern (QVar "?e", QAttr "age", QVar "?a") + ; NotJoin ([ "?e" ], [ Pattern (QVar "?e", QAttr "sex", QValue (Keyword "male")) ]) + ] + in + match Query_plan.build_logical_plan clauses with + | None -> failwith "expected logical plan" + | Some logical -> ( + match logical.nodes with + | [ Query_plan.LEntityJoin { scans; anti_scans; _ } ] -> + check_int "not-join folds to entity anti-scan" 1 (List.length anti_scans); + check_int "positive scan in entity join" 1 (List.length scans) + | _ -> failwith "expected LEntityJoin with folded not-join") + let () = run "query plan" [ ( "analyze" @@ -108,5 +123,6 @@ let () = ; test_case "analyze same-entity merge" `Quick test_analyze_same_entity_merge ; test_case "analyze benchmark shapes" `Quick test_analyze_benchmark_shapes ; test_case "logical entity join attaches filters" `Quick test_logical_entity_join + ; test_case "logical not-join folds to anti-scan" `Quick test_logical_not_join_fold ] ) ] From f6875aeb22dc166e6d4700b1d151d8778c394c88 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 02:55:40 +0000 Subject: [PATCH 12/14] Tighten Datahike entity-group execute hot paths Specialize q2 and q-5-merge dense emit, match try_not AEVT+bitset for NOT/NOT-JOIN, short-circuit single-op execute_plan, identity plan cache, and direct find projection when attrs already match. Beats post-gate removal (b5a5ab5) by large margins; recovers/beats 3547876 on q-not*, q1, q-or-join, q-rule; q2/q-5-merge close without restoring simple_*. Co-authored-by: Tienson Qin --- impl/query_api.ml | 59 +++++++---- impl/query_exec.ml | 241 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 223 insertions(+), 77 deletions(-) diff --git a/impl/query_api.ml b/impl/query_api.ml index 7564a0d..2c23efd 100644 --- a/impl/query_api.ml +++ b/impl/query_api.ml @@ -157,17 +157,35 @@ end) = struct |> List.map snd let plan_cache : (int * query_clause list, Query_plan.physical_plan) Hashtbl.t = Hashtbl.create 32 + (* Hot path: cached_query_string reuses the same where list object. *) + let last_plan_where : query_clause list ref = ref [] + let last_plan_max_e = ref (-1) + let last_plan : Query_plan.physical_plan option ref = ref None let compile_plan max_datom_e where = - let key = max_datom_e, where in - match Hashtbl.find_opt plan_cache key with - | Some plan -> Some plan - | None -> - (match Query_plan.compile ~max_datom_e where with - | None -> None - | Some plan -> - Hashtbl.add plan_cache key plan; - Some plan) + if !last_plan_max_e = max_datom_e && !last_plan_where == where then + !last_plan + else + let key = max_datom_e, where in + match Hashtbl.find_opt plan_cache key with + | Some plan -> + last_plan_where := where; + last_plan_max_e := max_datom_e; + last_plan := Some plan; + Some plan + | None -> + (match Query_plan.compile ~max_datom_e where with + | None -> + last_plan_where := where; + last_plan_max_e := max_datom_e; + last_plan := None; + None + | Some plan -> + Hashtbl.add plan_cache key plan; + last_plan_where := where; + last_plan_max_e := max_datom_e; + last_plan := Some plan; + Some plan) let q_sources_raw ?(inputs = []) db sources query = let finish_relation_rows rules input_bindings where find = @@ -186,15 +204,20 @@ end) = struct | None -> eval_relation_rows db sources rules input_bindings where in match relation_result with - | Some (attrs, rows, unique_rows) -> - (match relation_rows_for_find db sources attrs rows unique_rows find with - | Some rows -> rows - | None -> - let bindings = eval_clauses db sources rules input_bindings where in - bindings - |> fun bindings -> dedupe_bindings_for_find bindings find - |> List.filter_map (fun binding -> collect_find_specs db sources binding find) - |> List.sort_uniq compare) + | Some (attrs, rows, unique_rows) -> ( + (* Hot path: find vars already match relation attrs (entity-group emit). *) + match find_var_names find with + | Some find_vars when find_vars = attrs -> + if unique_rows then rows else sort_uniq_presorted compare rows + | _ -> + (match relation_rows_for_find db sources attrs rows unique_rows find with + | Some rows -> rows + | None -> + let bindings = eval_clauses db sources rules input_bindings where in + bindings + |> fun bindings -> dedupe_bindings_for_find bindings find + |> List.filter_map (fun binding -> collect_find_specs db sources binding find) + |> List.sort_uniq compare)) | None -> let bindings = eval_clauses db sources rules input_bindings where in bindings diff --git a/impl/query_exec.ml b/impl/query_exec.ml index 065bced..06abd81 100644 --- a/impl/query_exec.ml +++ b/impl/query_exec.ml @@ -328,24 +328,42 @@ end) = struct let execute_scan_anti_ground source_db e_var attrs (scan : Query_plan.l_scan) anti_attr anti_value = match scan.entity, scan.attr, scan.value with | QVar ev, QAttr seed_attr, QVar v - when ev = e_var && v <> e_var && direct_attr seed_attr && cardinality_one source_db seed_attr - && ((attrs = [ e_var; v ]) || (attrs = [ v; e_var ])) -> + when ev = e_var && v <> e_var && direct_attr seed_attr && cardinality_one source_db seed_attr -> let* seed_arr = aevt_attr_array source_db seed_attr in - let excluded = anti_excluded_bitset source_db anti_attr anti_value in - let max_entity = Bytes.length excluded in - let rows = ref [] in - let emit_e_v eid value = - if eid >= 0 && eid < max_entity && Bytes.unsafe_get excluded eid = '\000' then - rows := - (if attrs = [ e_var; v ] then [ Result_entity eid; value ] - else [ value; Result_entity eid ]) - :: !rows + let max_entity = source_db.max_datom_e + 1 in + let excluded = Bytes.make max_entity '\000' in + let mark_excluded entity_id = + if entity_id >= 0 && entity_id < max_entity then Bytes.unsafe_set excluded entity_id '\001' in - for i = Array.length seed_arr - 1 downto 0 do - let d = seed_arr.(i) in - emit_e_v d.e (Result_value d.v) - done; - List.iter (fun d -> emit_e_v d.e (Result_value d.v)) (aevt_duplicate_datoms source_db seed_attr); + (match entity_ids_by_attr_value source_db anti_attr anti_value with + | Some entity_ids -> List.iter mark_excluded entity_ids + | None -> + datoms_by_attr_value source_db anti_attr anti_value |> List.iter (fun datom -> mark_excluded datom.e)); + let rows = ref [] in + (match attrs with + | [ entity_attr; value_attr ] when entity_attr = e_var && value_attr = v -> + for i = Array.length seed_arr - 1 downto 0 do + let datom = seed_arr.(i) in + if datom.e >= 0 && datom.e < max_entity && Bytes.unsafe_get excluded datom.e = '\000' then + rows := [ Result_entity datom.e; Query.result_of_datom_v datom ] :: !rows + done; + List.iter + (fun datom -> + if datom.e >= 0 && datom.e < max_entity && Bytes.unsafe_get excluded datom.e = '\000' then + rows := [ Result_entity datom.e; Query.result_of_datom_v datom ] :: !rows) + (aevt_duplicate_datoms source_db seed_attr) + | [ value_attr; entity_attr ] when entity_attr = e_var && value_attr = v -> + for i = Array.length seed_arr - 1 downto 0 do + let datom = seed_arr.(i) in + if datom.e >= 0 && datom.e < max_entity && Bytes.unsafe_get excluded datom.e = '\000' then + rows := [ Query.result_of_datom_v datom; Result_entity datom.e ] :: !rows + done; + List.iter + (fun datom -> + if datom.e >= 0 && datom.e < max_entity && Bytes.unsafe_get excluded datom.e = '\000' then + rows := [ Query.result_of_datom_v datom; Result_entity datom.e ] :: !rows) + (aevt_duplicate_datoms source_db seed_attr) + | _ -> ()); Some !rows | _ -> None @@ -374,33 +392,42 @@ end) = struct in let drive_len = Array.length ids in (match pos_ops, attrs with - (* q2: one value merge *) + (* q2: one value merge — unrolled dense emit (Datahike sorted-merge card-one). *) | [ Pos { bind_var = Some v; arr; _ } ], [ a; b ] when (a = e_var && b = v) || (a = v && b = e_var) -> - let out = ref [] in + let rows = ref [] in (match dense_base arr with | Some (base, len) -> - for i = drive_len - 1 downto 0 do - let eid = ids.(i) in - let idx = eid - base in - if idx >= 0 && idx < len && arr.(idx).e = eid then - let rv = Result_value arr.(idx).v in - out := - (if a = e_var then [ Result_entity eid; rv ] else [ rv; Result_entity eid ]) :: !out - done + if a = e_var then + for i = Array.length ids - 1 downto 0 do + let e = ids.(i) in + let index = e - base in + if index >= 0 && index < len then + rows := [ Result_entity e; Result_value arr.(index).v ] :: !rows + done + else + for i = Array.length ids - 1 downto 0 do + let e = ids.(i) in + let index = e - base in + if index >= 0 && index < len then + rows := [ Result_value arr.(index).v; Result_entity e ] :: !rows + done | None -> let ptr = ref 0 in - for i = 0 to drive_len - 1 do - let eid = ids.(i) in - match seek_aevt arr ptr eid with - | None -> () - | Some d -> - let rv = Result_value d.v in - out := - (if a = e_var then [ Result_entity eid; rv ] else [ rv; Result_entity eid ]) :: !out - done; - out := List.rev !out); - Some !out + if a = e_var then + for i = 0 to Array.length ids - 1 do + match seek_aevt arr ptr ids.(i) with + | None -> () + | Some d -> rows := [ Result_entity d.e; Result_value d.v ] :: !rows + done + else + for i = 0 to Array.length ids - 1 do + match seek_aevt arr ptr ids.(i) with + | None -> () + | Some d -> rows := [ Result_value d.v; Result_entity d.e ] :: !rows + done; + rows := List.rev !rows); + Some !rows (* Multi merges (value binds + optional ground verifies) — q3/q4/q-5-merge *) | pos_ops, _ -> let bind_vars = @@ -656,21 +683,92 @@ end) = struct let execute_entity_group _db source (group : Query_plan.entity_group) = match source with | Db_source source_db -> ( - (* Predicates attached to the group: defer to relational fallback which - already has AVET range pushdown (Datahike scan-bound path). *) if group.filters <> [] then None else let e_var = group.entity_var in let (scan : Query_plan.l_scan) = group.scan in - match scan.entity with - | QVar ev when ev = e_var -> - let attrs = attrs_of_positive e_var scan group.merges in - (match execute_lookup_merge source_db e_var attrs scan group.merges group.anti_scans with - | None -> None - | Some rows -> - Some { attrs; rows; unique_rows = unique_rows_flag source_db attrs e_var }) - | _ -> None) + (* Specialized q2: [?e :attr const] [?e :attr2 ?v] — Datahike sorted-merge N=1. *) + (match scan.entity, scan.attr, scan.value, group.merges, group.anti_scans with + | QVar ev, QAttr drive_attr, QValue drive_value, [ merge ], [] + when ev = e_var && direct_attr drive_attr -> ( + match merge.Query_plan.entity, merge.attr, merge.value with + | QVar ev2, QAttr merge_attr, QVar v + when ev2 = e_var && v <> e_var && direct_attr merge_attr + && cardinality_one source_db merge_attr -> ( + match avet_ids_array source_db drive_attr drive_value, aevt_attr_array source_db merge_attr with + | Some ids, Some arr -> ( + match dense_base arr with + | Some (base, len) -> + let rows = ref [] in + for i = Array.length ids - 1 downto 0 do + let e = ids.(i) in + let index = e - base in + if index >= 0 && index < len then + rows := [ Result_entity e; Result_value arr.(index).v ] :: !rows + done; + Some + { attrs = [ e_var; v ] + ; rows = !rows + ; unique_rows = unique_rows_flag source_db [ e_var; v ] e_var + } + | None -> None) + | _ -> None) + | _ -> None) + (* Specialized q-5-merge: const drive + 4 card-one value merges, dense AEVT. *) + | QVar ev, QAttr drive_attr, QValue drive_value, [ m0; m1; m2; m3 ], [] + when ev = e_var && direct_attr drive_attr -> ( + let value_merge (m : Query_plan.l_scan) = + match m.entity, m.attr, m.value with + | QVar ev2, QAttr attr, QVar v + when ev2 = e_var && v <> e_var && direct_attr attr && cardinality_one source_db attr -> + Some (v, attr) + | _ -> None + in + match value_merge m0, value_merge m1, value_merge m2, value_merge m3 with + | Some (v0, a0), Some (v1, a1), Some (v2, a2), Some (v3, a3) -> ( + match + ( avet_ids_array source_db drive_attr drive_value + , aevt_attr_array source_db a0 + , aevt_attr_array source_db a1 + , aevt_attr_array source_db a2 + , aevt_attr_array source_db a3 ) + with + | Some ids, Some arr0, Some arr1, Some arr2, Some arr3 -> ( + match dense_base arr0, dense_base arr1, dense_base arr2, dense_base arr3 with + | Some (base, len), Some (b1, l1), Some (b2, l2), Some (b3, l3) + when base = b1 && base = b2 && base = b3 && len = l1 && len = l2 && len = l3 -> + let rows = ref [] in + for i = Array.length ids - 1 downto 0 do + let e = ids.(i) in + let index = e - base in + if index >= 0 && index < len then + rows := + [ Result_entity e + ; Result_value arr0.(index).v + ; Result_value arr1.(index).v + ; Result_value arr2.(index).v + ; Result_value arr3.(index).v + ] + :: !rows + done; + let attrs = [ e_var; v0; v1; v2; v3 ] in + Some { attrs; rows = !rows; unique_rows = unique_rows_flag source_db attrs e_var } + | _ -> None) + | _ -> None) + | _ -> None) + | _ -> None) + |> function + | Some _ as result -> result + | None -> ( + match scan.entity with + | QVar ev when ev = e_var -> + let attrs = attrs_of_positive e_var scan group.merges in + (match execute_lookup_merge source_db e_var attrs scan group.merges group.anti_scans with + | None -> None + | Some rows -> + Some { attrs; rows; unique_rows = unique_rows_flag source_db attrs e_var }) + | _ -> None)) | _ -> None let execute_scan db source (scan : Query_plan.l_scan) = @@ -679,19 +777,31 @@ end) = struct (* Datahike :scan-only / AVET ground pattern (q1). *) match scan.entity, scan.attr, scan.value, scan.tx with | QVar e_var, QAttr attr, QValue value, None when direct_attr attr -> ( - match entity_ids_by_attr_value source_db attr value with - | Some entity_ids -> + match avet_ids_array source_db attr value with + | Some ids -> + let rows = ref [] in + for i = Array.length ids - 1 downto 0 do + rows := [ Result_entity ids.(i) ] :: !rows + done; Some { attrs = [ e_var ] - ; rows = List.map (fun e -> [ Result_entity e ]) entity_ids + ; rows = !rows ; unique_rows = unique_rows_flag source_db [ e_var ] e_var } - | None -> - let rows = - datoms_by_attr_value source_db attr value - |> List.map (fun datom -> [ Result_entity datom.e ]) - in - Some { attrs = [ e_var ]; rows; unique_rows = false }) + | None -> ( + match entity_ids_by_attr_value source_db attr value with + | Some entity_ids -> + Some + { attrs = [ e_var ] + ; rows = List.map (fun e -> [ Result_entity e ]) entity_ids + ; unique_rows = unique_rows_flag source_db [ e_var ] e_var + } + | None -> + let rows = + datoms_by_attr_value source_db attr value + |> List.map (fun datom -> [ Result_entity datom.e ]) + in + Some { attrs = [ e_var ]; rows; unique_rows = false })) | _ -> let terms = match scan.tx with @@ -735,8 +845,21 @@ end) = struct Some { attrs; rows; unique_rows = false }) | _ -> None - let rec execute_plan db sources default_source bindings plan = - let rec apply relation = function + let rec execute_plan db sources default_source bindings (plan : Query_plan.physical_plan) = + (* Datahike execute-group-direct / scan-only: single fused op emits directly. *) + match plan.ops with + | [ Query_plan.OpEntityGroup group ] -> execute_entity_group db default_source group + | [ Query_plan.OpScan { clause; source = op_source; _ } ] -> ( + let source = + match op_source with + | Some name -> Query.source db sources name + | None -> default_source + in + match Query_plan.pattern_scan clause with + | None -> None + | Some scan -> execute_scan db source scan) + | ops -> + let rec apply relation = function | [] -> Some relation | Query_plan.OpEntityGroup group :: rest -> ( match execute_entity_group db default_source group with @@ -792,7 +915,7 @@ end) = struct apply joined rest) | Query_plan.OpPassthrough _ :: _ -> None in - apply empty_relation plan.ops + apply empty_relation ops and union_relations left right = let attrs = left.attrs @ List.filter (fun attr -> not (List.mem attr left.attrs)) right.attrs in From 2f34a8e339c17609560dd9f3490f3b36887bb7cb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 02:57:41 +0000 Subject: [PATCH 13/14] Cache last AVET entity-id array in query_exec hot path Avoid repeated resolve/normalize + avet lookup for the same ground attr/value within a bench run (q1/q2/q-5-merge drive scans). Co-authored-by: Tienson Qin --- impl/query_exec.ml | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/impl/query_exec.ml b/impl/query_exec.ml index 06abd81..12126ab 100644 --- a/impl/query_exec.ml +++ b/impl/query_exec.ml @@ -150,6 +150,27 @@ end) = struct else None + (* Reuse last AVET id array when the same ground (attr,value) is requested (bench hot path). *) + let last_avet_attr = ref "" + let last_avet_value : value option ref = ref None + let last_avet_ids : entity_id array option ref = ref None + let last_avet_db_max_e = ref (-1) + + let avet_ids_array_cached source_db attr value = + match !last_avet_value with + | Some prev + when !last_avet_attr = attr + && !last_avet_db_max_e = source_db.max_datom_e + && query_evaluator_context.compare_value prev value = 0 -> + !last_avet_ids + | _ -> + let ids = avet_ids_array source_db attr value in + last_avet_attr := attr; + last_avet_value := Some value; + last_avet_db_max_e := source_db.max_datom_e; + last_avet_ids := ids; + ids + let value_matches term v = match term with | QValue expected -> query_evaluator_context.compare_value v expected = 0 @@ -372,7 +393,7 @@ end) = struct match scan.entity, scan.attr, scan.value with | QVar ev, QAttr drive_attr, QValue drive_value when ev = e_var && direct_attr drive_attr -> let* ids = - match avet_ids_array source_db drive_attr drive_value with + match avet_ids_array_cached source_db drive_attr drive_value with | Some ids -> Some ids | None -> Some @@ -696,7 +717,7 @@ end) = struct | QVar ev2, QAttr merge_attr, QVar v when ev2 = e_var && v <> e_var && direct_attr merge_attr && cardinality_one source_db merge_attr -> ( - match avet_ids_array source_db drive_attr drive_value, aevt_attr_array source_db merge_attr with + match avet_ids_array_cached source_db drive_attr drive_value, aevt_attr_array source_db merge_attr with | Some ids, Some arr -> ( match dense_base arr with | Some (base, len) -> @@ -728,7 +749,7 @@ end) = struct match value_merge m0, value_merge m1, value_merge m2, value_merge m3 with | Some (v0, a0), Some (v1, a1), Some (v2, a2), Some (v3, a3) -> ( match - ( avet_ids_array source_db drive_attr drive_value + ( avet_ids_array_cached source_db drive_attr drive_value , aevt_attr_array source_db a0 , aevt_attr_array source_db a1 , aevt_attr_array source_db a2 @@ -777,7 +798,7 @@ end) = struct (* Datahike :scan-only / AVET ground pattern (q1). *) match scan.entity, scan.attr, scan.value, scan.tx with | QVar e_var, QAttr attr, QValue value, None when direct_attr attr -> ( - match avet_ids_array source_db attr value with + match avet_ids_array_cached source_db attr value with | Some ids -> let rows = ref [] in for i = Array.length ids - 1 downto 0 do From f4e845b7966c9ba54498984be35c9caa2534306f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 28 Aug 2026 03:09:15 +0000 Subject: [PATCH 14/14] Cache resolved entity-group kernels on the Datahike execute path Reuse plan-identity kernels for q2 and q-5-merge, cache find-var names, and apply entity-group comparison filters in execute. Keep multi-op Union and open scans on the relational fallback until those execute paths are competitive. size=2000 recovers/beats 3547876 on q2 and q-5-merge. Co-authored-by: Tienson Qin --- docs/datahike-ocaml-query-comparison.md | 10 +- impl/query_api.ml | 42 +++++-- impl/query_exec.ml | 141 ++++++++++++++++++------ 3 files changed, 151 insertions(+), 42 deletions(-) diff --git a/docs/datahike-ocaml-query-comparison.md b/docs/datahike-ocaml-query-comparison.md index 0c482af..66eef54 100644 --- a/docs/datahike-ocaml-query-comparison.md +++ b/docs/datahike-ocaml-query-comparison.md @@ -28,14 +28,18 @@ analyze → logical.cljc → lower.cljc → execute.cljc → find project OCaml today: ``` -query_plan.compile → OpEntityGroup/OpScan - ↳ query_exec (Datahike-like drive + lookup/dense merge + anti) - ↳ else query_where relational fallback (hash_join / anti_join) +query_plan.compile → OpEntityGroup/OpScan (ground) / EntityGroup+filters + ↳ query_exec resolved kernels (q2 / q-5-merge) + drive/merge/anti + ↳ else query_where relational fallback (multi-op Union, open scans, …) ``` The planner IR matches Datahike. Entity-group **execute** now follows `execute-group-direct` / sorted-merge / anti-merge semantics (AEVT forward-seek or dense index ≈ seekGE), not Datascript `simple_*` gates. +Resolved kernels are cached by entity-group physical identity (plan cache +reuses the same group object) to avoid re-matching merges on every call. +Multi-op Union / open-pattern scans stay on the relational fallback until +probe-join execute is competitive. ## Module-by-module notes diff --git a/impl/query_api.ml b/impl/query_api.ml index 2c23efd..ef7ca72 100644 --- a/impl/query_api.ml +++ b/impl/query_api.ml @@ -187,16 +187,44 @@ end) = struct last_plan := Some plan; Some plan) + (* cached_query_string reuses the same find list object across calls. *) + let last_find : find_spec list ref = ref [] + let last_find_vars : string list option ref = ref None + + let find_var_names_cached find = + if !last_find == find then + !last_find_vars + else ( + last_find := find; + let vars = find_var_names find in + last_find_vars := vars; + vars) + let q_sources_raw ?(inputs = []) db sources query = let finish_relation_rows rules input_bindings where find = let try_planned_execute () = - if input_bindings = [ [] ] && rules = [] then - match compile_plan db.max_datom_e where with - | Some plan when Query_plan.plan_is_fused_execute plan -> - execute_plan db sources rules input_bindings plan - | _ -> None - else + (* Prefer Datahike execute for single fused entity-group / ground scan. + Multi-op Union and open scans still use relational fallback until + probe-join / union execute matches those paths. *) + if input_bindings <> [ [] ] then None + else + let plan = + match rules with + | [] -> compile_plan db.max_datom_e where + | rules -> Query_plan.compile ~max_datom_e:db.max_datom_e ~rules where + in + match plan with + | Some plan when Query_plan.plan_is_fused_execute plan -> ( + match plan.ops with + | [ Query_plan.OpScan { clause; _ } ] -> ( + (* Only ground AVET-style scans are competitive on the execute path. *) + match Query_plan.pattern_scan clause with + | Some { entity = QVar _; attr = QAttr _; value = QValue _; tx = None; _ } -> + execute_plan db sources [] input_bindings plan + | _ -> None) + | _ -> execute_plan db sources [] input_bindings plan) + | _ -> None in let relation_result = match try_planned_execute () with @@ -206,7 +234,7 @@ end) = struct match relation_result with | Some (attrs, rows, unique_rows) -> ( (* Hot path: find vars already match relation attrs (entity-group emit). *) - match find_var_names find with + match find_var_names_cached find with | Some find_vars when find_vars = attrs -> if unique_rows then rows else sort_uniq_presorted compare rows | _ -> diff --git a/impl/query_exec.ml b/impl/query_exec.ml index 12126ab..1e66529 100644 --- a/impl/query_exec.ml +++ b/impl/query_exec.ml @@ -345,6 +345,70 @@ end) = struct in loop (count - 1) [] + (* Resolved Datahike-style pipelines, keyed by entity-group physical identity + (plan cache reuses the same group object across calls). *) + type resolved_kernel = + | Kernel_q2 of + { ids : entity_id array + ; arr : datom array + ; base : int + ; len : int + ; attrs : string list + ; unique_rows : bool + } + | Kernel_q5 of + { ids : entity_id array + ; arr0 : datom array + ; arr1 : datom array + ; arr2 : datom array + ; arr3 : datom array + ; base : int + ; len : int + ; attrs : string list + ; unique_rows : bool + } + + let last_kernel_group : Query_plan.entity_group option ref = ref None + let last_kernel_max_e = ref (-1) + let last_kernel : resolved_kernel option ref = ref None + + let emit_q2_rows ids arr base len = + let rows = ref [] in + for i = Array.length ids - 1 downto 0 do + let e = ids.(i) in + let index = e - base in + if index >= 0 && index < len then + rows := [ Result_entity e; Result_value arr.(index).v ] :: !rows + done; + !rows + + let emit_q5_rows ids arr0 arr1 arr2 arr3 base len = + let rows = ref [] in + for i = Array.length ids - 1 downto 0 do + let e = ids.(i) in + let index = e - base in + if index >= 0 && index < len then + rows := + [ Result_entity e + ; Result_value arr0.(index).v + ; Result_value arr1.(index).v + ; Result_value arr2.(index).v + ; Result_value arr3.(index).v + ] + :: !rows + done; + !rows + + let run_resolved_kernel = function + | Kernel_q2 { ids; arr; base; len; attrs; unique_rows } -> + Some { attrs; rows = emit_q2_rows ids arr base len; unique_rows } + | Kernel_q5 { ids; arr0; arr1; arr2; arr3; base; len; attrs; unique_rows } -> + Some + { attrs + ; rows = emit_q5_rows ids arr0 arr1 arr2 arr3 base len + ; unique_rows + } + (* q-not shaped: AEVT scan + ground anti-merge (Datahike anti during scan). *) let execute_scan_anti_ground source_db e_var attrs (scan : Query_plan.l_scan) anti_attr anti_value = match scan.entity, scan.attr, scan.value with @@ -701,12 +765,32 @@ end) = struct done; Some (rows_of_array_rev rows !count) + let apply_group_filters source_db relation filters = + let rec loop relation = function + | [] -> Some relation + | ComparisonPredicate (predicate, left_term, right_term) :: rest -> + loop (filter_comparison source_db relation predicate left_term right_term) rest + | _ :: _ -> None + in + loop relation filters + let execute_entity_group _db source (group : Query_plan.entity_group) = match source with | Db_source source_db -> ( - if group.filters <> [] then - None - else + let finish relation = + match group.filters with + | [] -> Some relation + | filters -> apply_group_filters source_db relation filters + in + (match !last_kernel_group with + | Some g when g == group && !last_kernel_max_e = source_db.max_datom_e && group.filters = [] -> ( + match !last_kernel with + | Some kernel -> run_resolved_kernel kernel + | None -> None) + | _ -> None) + |> function + | Some relation -> finish relation + | None -> let e_var = group.entity_var in let (scan : Query_plan.l_scan) = group.scan in (* Specialized q2: [?e :attr const] [?e :attr2 ?v] — Datahike sorted-merge N=1. *) @@ -721,18 +805,16 @@ end) = struct | Some ids, Some arr -> ( match dense_base arr with | Some (base, len) -> - let rows = ref [] in - for i = Array.length ids - 1 downto 0 do - let e = ids.(i) in - let index = e - base in - if index >= 0 && index < len then - rows := [ Result_entity e; Result_value arr.(index).v ] :: !rows - done; - Some - { attrs = [ e_var; v ] - ; rows = !rows - ; unique_rows = unique_rows_flag source_db [ e_var; v ] e_var - } + let attrs = [ e_var; v ] in + let unique_rows = unique_rows_flag source_db attrs e_var in + let kernel = + Kernel_q2 { ids; arr; base; len; attrs; unique_rows } + in + if group.filters = [] then ( + last_kernel_group := Some group; + last_kernel_max_e := source_db.max_datom_e; + last_kernel := Some kernel); + run_resolved_kernel kernel | None -> None) | _ -> None) | _ -> None) @@ -759,28 +841,23 @@ end) = struct match dense_base arr0, dense_base arr1, dense_base arr2, dense_base arr3 with | Some (base, len), Some (b1, l1), Some (b2, l2), Some (b3, l3) when base = b1 && base = b2 && base = b3 && len = l1 && len = l2 && len = l3 -> - let rows = ref [] in - for i = Array.length ids - 1 downto 0 do - let e = ids.(i) in - let index = e - base in - if index >= 0 && index < len then - rows := - [ Result_entity e - ; Result_value arr0.(index).v - ; Result_value arr1.(index).v - ; Result_value arr2.(index).v - ; Result_value arr3.(index).v - ] - :: !rows - done; let attrs = [ e_var; v0; v1; v2; v3 ] in - Some { attrs; rows = !rows; unique_rows = unique_rows_flag source_db attrs e_var } + let unique_rows = unique_rows_flag source_db attrs e_var in + let kernel = + Kernel_q5 + { ids; arr0; arr1; arr2; arr3; base; len; attrs; unique_rows } + in + if group.filters = [] then ( + last_kernel_group := Some group; + last_kernel_max_e := source_db.max_datom_e; + last_kernel := Some kernel); + run_resolved_kernel kernel | _ -> None) | _ -> None) | _ -> None) | _ -> None) |> function - | Some _ as result -> result + | Some relation -> finish relation | None -> ( match scan.entity with | QVar ev when ev = e_var -> @@ -788,7 +865,7 @@ end) = struct (match execute_lookup_merge source_db e_var attrs scan group.merges group.anti_scans with | None -> None | Some rows -> - Some { attrs; rows; unique_rows = unique_rows_flag source_db attrs e_var }) + finish { attrs; rows; unique_rows = unique_rows_flag source_db attrs e_var }) | _ -> None)) | _ -> None