|
9 | 9 | import io.brackit.query.compiler.AST; |
10 | 10 | import io.brackit.query.compiler.XQ; |
11 | 11 | import io.brackit.query.compiler.optimizer.PredicateNode; |
| 12 | +import io.brackit.query.compiler.optimizer.SourceRef; |
12 | 13 | import io.brackit.query.compiler.optimizer.Stage; |
13 | 14 | import io.brackit.query.compiler.optimizer.VectorizedScanAnnotation; |
| 15 | +import io.brackit.query.function.json.JSONFun; |
14 | 16 | import io.brackit.query.module.StaticContext; |
15 | 17 |
|
16 | 18 | import java.util.ArrayList; |
| 19 | +import java.util.HashMap; |
| 20 | +import java.util.HashSet; |
17 | 21 | import java.util.List; |
| 22 | +import java.util.Map; |
| 23 | +import java.util.Set; |
18 | 24 |
|
19 | 25 | /** |
20 | 26 | * Optimizer stage that detects FLWOR patterns eligible for vectorized execution. |
|
43 | 49 | */ |
44 | 50 | public final class VectorizedGroupByDetection implements Stage { |
45 | 51 |
|
| 52 | + /** Guard against pathological ASTs when resolving a scan source through variable bindings. */ |
| 53 | + private static final int MAX_UNWRAP_STEPS = 64; |
| 54 | + |
46 | 55 | @Override |
47 | 56 | public AST rewrite(StaticContext sctx, AST ast) { |
48 | | - walkAndAnnotate(ast); |
| 57 | + // Resolve a scan's source document (a `for $u in $doc[]` reaches its `jn:doc(...)` only through the |
| 58 | + // `let $doc := ...` binding), so collect every visible for/let binding once up front and thread the |
| 59 | + // map into the per-PipeExpr annotation. |
| 60 | + final Map<Object, AST> variableBindings = new HashMap<>(); |
| 61 | + collectVariableBindings(ast, variableBindings); |
| 62 | + walkAndAnnotate(ast, variableBindings); |
49 | 63 | return ast; |
50 | 64 | } |
51 | 65 |
|
52 | | - private void walkAndAnnotate(AST node) { |
| 66 | + private void walkAndAnnotate(AST node, Map<Object, AST> variableBindings) { |
53 | 67 | if (node == null) |
54 | 68 | return; |
55 | 69 | if (node.getType() == XQ.PipeExpr) { |
56 | | - tryAnnotate(node); |
| 70 | + tryAnnotate(node, variableBindings); |
57 | 71 | } |
58 | 72 | for (int i = 0; i < node.getChildCount(); i++) { |
59 | | - walkAndAnnotate(node.getChild(i)); |
| 73 | + walkAndAnnotate(node.getChild(i), variableBindings); |
60 | 74 | } |
61 | 75 | } |
62 | 76 |
|
63 | 77 | // ==================== Main pattern matcher ==================== |
64 | 78 |
|
65 | | - private void tryAnnotate(AST pipeExpr) { |
| 79 | + private void tryAnnotate(AST pipeExpr, Map<Object, AST> variableBindings) { |
66 | 80 | if (pipeExpr.getChildCount() < 1) |
67 | 81 | return; |
68 | 82 | AST chain = pipeExpr.getChild(0); |
@@ -237,6 +251,11 @@ private void tryAnnotate(AST pipeExpr) { |
237 | 251 | if (sourcePath != null) { |
238 | 252 | pipeExpr.setProperty(VectorizedScanAnnotation.SOURCE_PATH_PREFIX, sourcePath); |
239 | 253 | } |
| 254 | + // Document identity of the scan source. Set unconditionally (the translator only consults it once |
| 255 | + // a vectorized claim exists) so a resource-bound executor can decline a scan over a document it is |
| 256 | + // not bound to — see VectorizedExecutor#acceptsSource. |
| 257 | + pipeExpr.setProperty(VectorizedScanAnnotation.SOURCE_REF, |
| 258 | + resolveSourceRef(forBind.getChild(1), variableBindings)); |
240 | 259 | } |
241 | 260 |
|
242 | 261 | // Sorted scan emits FULL RECORDS sorted by ONE direct `$loopVar.field` key — only |
@@ -564,6 +583,124 @@ private static String qnmLocalName(final Object value) { |
564 | 583 | return null; |
565 | 584 | } |
566 | 585 |
|
| 586 | + // ==================== Source-document identity extraction ==================== |
| 587 | + |
| 588 | + /** |
| 589 | + * Collect every {@link XQ#ForBind}/{@link XQ#LetBind} binding in the tree into {@code out}, keyed by |
| 590 | + * the declared variable's QNm. First (outermost) binding wins on shadowing — a heuristic that only |
| 591 | + * ever costs precision (a misresolved source yields {@link SourceRef#unknown()}, which fails closed). |
| 592 | + */ |
| 593 | + private static void collectVariableBindings(final AST node, final Map<Object, AST> out) { |
| 594 | + if ((node.getType() == XQ.ForBind || node.getType() == XQ.LetBind) && node.getChildCount() >= 2) { |
| 595 | + final Object varKey = bindingVariableKey(node.getChild(0)); |
| 596 | + if (varKey != null) { |
| 597 | + out.putIfAbsent(varKey, node.getChild(1)); |
| 598 | + } |
| 599 | + } |
| 600 | + for (int i = 0, n = node.getChildCount(); i < n; i++) { |
| 601 | + collectVariableBindings(node.getChild(i), out); |
| 602 | + } |
| 603 | + } |
| 604 | + |
| 605 | + /** |
| 606 | + * The variable QNm bound by a {@code For}/{@code LetBind}'s first child (a |
| 607 | + * {@link XQ#TypedVariableBinding} whose own first child, the {@code Variable}, carries the QNm that a |
| 608 | + * {@link XQ#VariableRef} later resolves against). Falls back to the node's own value defensively. |
| 609 | + */ |
| 610 | + private static Object bindingVariableKey(final AST typedVariableBinding) { |
| 611 | + if (typedVariableBinding.getChildCount() > 0) { |
| 612 | + return typedVariableBinding.getChild(0).getValue(); |
| 613 | + } |
| 614 | + return typedVariableBinding.getValue(); |
| 615 | + } |
| 616 | + |
| 617 | + /** |
| 618 | + * Resolve a loop variable's source expression down to the document it reads from, following |
| 619 | + * deref/array/filter layers and variable bindings, and classify it as a {@link SourceRef}. Never |
| 620 | + * {@code null}: an unresolvable, dynamic, cyclic, collection, or non-document source resolves to |
| 621 | + * {@link SourceRef#unknown()} so a resource-bound executor fails closed. |
| 622 | + */ |
| 623 | + private SourceRef resolveSourceRef(final AST binding, final Map<Object, AST> variableBindings) { |
| 624 | + final Set<Object> resolvingVars = new HashSet<>(4); |
| 625 | + AST current = binding; |
| 626 | + for (int step = 0; current != null && step < MAX_UNWRAP_STEPS; step++) { |
| 627 | + switch (current.getType()) { |
| 628 | + case XQ.DerefExpr, XQ.ArrayAccess, XQ.FilterExpr -> { |
| 629 | + if (current.getChildCount() < 1) { |
| 630 | + return SourceRef.unknown(); |
| 631 | + } |
| 632 | + current = current.getChild(0); |
| 633 | + } |
| 634 | + case XQ.VariableRef -> { |
| 635 | + final Object varKey = current.getValue(); |
| 636 | + if (varKey == null || !resolvingVars.add(varKey)) { |
| 637 | + return SourceRef.unknown(); // unresolved or cyclic — cannot prove a single document |
| 638 | + } |
| 639 | + final AST resolved = variableBindings.get(varKey); |
| 640 | + if (resolved == null) { |
| 641 | + return SourceRef.unknown(); // a for-loop / outer variable, not a document binding |
| 642 | + } |
| 643 | + current = resolved; |
| 644 | + } |
| 645 | + case XQ.ContextItemExpr -> { |
| 646 | + return SourceRef.contextItem(); // the caller's own bound read transaction |
| 647 | + } |
| 648 | + case XQ.FunctionCall -> { |
| 649 | + return functionCallSourceRef(current); |
| 650 | + } |
| 651 | + default -> { |
| 652 | + return SourceRef.unknown(); |
| 653 | + } |
| 654 | + } |
| 655 | + } |
| 656 | + return SourceRef.unknown(); |
| 657 | + } |
| 658 | + |
| 659 | + /** |
| 660 | + * Classify a {@link XQ#FunctionCall} scan source. A {@code jn:doc}/{@code jn:open} with literal |
| 661 | + * database and resource arguments (and, if present, a literal integer revision) yields a concrete |
| 662 | + * {@link SourceRef#document}; a dynamic argument, any other {@code jn:} opener (collection / |
| 663 | + * multi-revision — it spans more than one resource/revision), or a non-JSON function yields |
| 664 | + * {@link SourceRef#unknown()}. |
| 665 | + */ |
| 666 | + private SourceRef functionCallSourceRef(final AST call) { |
| 667 | + if (!(call.getValue() instanceof QNm qnm) || !JSONFun.JSON_NSURI.equals(qnm.getNamespaceURI())) { |
| 668 | + return SourceRef.unknown(); |
| 669 | + } |
| 670 | + final String local = qnm.getLocalName(); |
| 671 | + if (!"doc".equals(local) && !"open".equals(local)) { |
| 672 | + return SourceRef.unknown(); |
| 673 | + } |
| 674 | + if (call.getChildCount() < 2) { |
| 675 | + return SourceRef.unknown(); |
| 676 | + } |
| 677 | + final String databaseName = stringLiteralValue(call.getChild(0)); |
| 678 | + final String resourceName = stringLiteralValue(call.getChild(1)); |
| 679 | + if (databaseName == null || resourceName == null) { |
| 680 | + return SourceRef.unknown(); // dynamic (non-literal) database/resource — unprovable |
| 681 | + } |
| 682 | + if (call.getChildCount() == 2) { |
| 683 | + return SourceRef.document(databaseName, resourceName, SourceRef.LATEST_REVISION); |
| 684 | + } |
| 685 | + final Integer revision = literalRevision(call.getChild(2)); |
| 686 | + if (revision == null) { |
| 687 | + return SourceRef.unknown(); // dynamic revision — unprovable |
| 688 | + } |
| 689 | + return SourceRef.document(databaseName, resourceName, revision); |
| 690 | + } |
| 691 | + |
| 692 | + /** The exact int of a literal integer revision argument; {@code null} for anything non-literal/lossy. */ |
| 693 | + private static Integer literalRevision(final AST node) { |
| 694 | + if (node == null || node.getType() != XQ.Int) { |
| 695 | + return null; |
| 696 | + } |
| 697 | + final Long lv = exactLongOf(node.getValue()); |
| 698 | + if (lv == null || lv < Integer.MIN_VALUE || lv > Integer.MAX_VALUE) { |
| 699 | + return null; |
| 700 | + } |
| 701 | + return lv.intValue(); |
| 702 | + } |
| 703 | + |
567 | 704 | // ==================== Generic predicate-tree extraction ==================== |
568 | 705 |
|
569 | 706 | /** |
|
0 commit comments