Skip to content
44 changes: 44 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Session Notes

## Namespace and parent-pointer findings

- Parsed XML elements need `inheritNamespacesFromParent = true` in `src/main/java/org/rumbledb/items/xml/ElementItem.java` so that inherited namespaces from loaded documents remain visible to functions that depend on in-scope namespaces.
- Parent-pointer optimization must be disabled conservatively for queries that need ancestor namespace context. The current centralized mechanism is `src/main/java/org/rumbledb/compiler/ParentPointerAnalysisVisitor.java`, called from `src/main/java/org/rumbledb/compiler/VisitorHelpers.java`.
- The analysis must recognize both `Name.FN_NS` and `Name.JSONIQ_DEFAULT_FUNCTION_NS`, otherwise unprefixed builtin calls in JSONiq can be missed.
- The validated guarded functions are:
- `fn:lang#1`, `fn:lang#2`
- `fn:in-scope-prefixes#1`
- `fn:namespace-uri-for-prefix#2`
- `fn:serialize#1`, `fn:serialize#2`
- `fn:innermost#1`
- `fn:outermost#1`

## Important A/B result

- On July 23, 2026, `fn:serialize` was tested with an explicit A/B rebuild.
- With `serialize#1/#2` removed from `ParentPointerAnalysisVisitor`, the three QT3 queries below all lost the expected XML 1.1 namespace undeclaration `xmlns:p=""`:
- `fn/serialize.xml:serialize-xml-035`
- `fn/serialize.xml:serialize-xml-035b`
- `fn/serialize.xml:serialize-xml-135`
- After restoring the `serialize` guard, direct `spark-submit` execution again produced the correct serialized output containing `section xmlns:p=""`.
- Conclusion: `fn:serialize` really does depend on ancestor namespace context in these cases, so keeping it in the parent-pointer guard set is required.

## Report interpretation note

- A stale `xquery-tests.html` can disagree with the current jar. When a regression looks suspicious, verify it with direct `spark-submit` execution against the actual rebuilt jar in `target/rumbledb-2.1.0-jar-with-dependencies.jar`.
- For this repo, do not rely on `mvn compile` alone when you need to refresh the runnable jar.
- The rebuild command that reliably refreshes the runnable jar is:

```sh
mvn clean compile assembly:single
```

## Useful direct checks used in this session

```sh
spark-submit target/rumbledb-2.1.0-jar-with-dependencies.jar run --default-language xquery31 -q 'string-join(in-scope-prefixes((doc("file:///Users/ghislain/Code/rumble-test-suite/qt3tests/docs/auction.xml")//*)[19]), "|")'
```

```sh
spark-submit target/rumbledb-2.1.0-jar-with-dependencies.jar run --default-language xquery31 -q 'let $d := doc("file:///Users/ghislain/Code/rumble-test-suite/qt3tests/fn/serialize/serialize-035-src.xml") let $params := map {"method" : "xml", "version" : "1.1", "undeclare-prefixes" : true()} return serialize($d, $params)'
```
2 changes: 2 additions & 0 deletions src/main/java/org/rumbledb/compiler/CloneVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ public Node visitLibraryModule(LibraryModule module, Node argument) {
module.getMetadata()
);
result.setStaticContext(module.getStaticContext());
result.setLocation(module.getLocation());
result.setModuleIdentity(module.getModuleIdentity());
return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,19 +367,20 @@ public DynamicContext visitTypeDeclaration(TypeDeclaration declaration, DynamicC

@Override
public DynamicContext visitLibraryModule(LibraryModule module, DynamicContext argument) {
if (!this.importedModuleContexts.containsKey(module.getNamespace())) {
String moduleLocation = module.getModuleIdentityOrLocation();
if (!this.importedModuleContexts.containsKey(moduleLocation)) {
DynamicContext newContext = new DynamicContext(this.configuration, this.externalBindings);
newContext.setNamedFunctions(argument.getNamedFunctions());
DynamicContext importedContext = visitDescendants(module, newContext);
this.importedModuleContexts.put(module.getNamespace(), importedContext);
this.importedModuleContexts.put(moduleLocation, importedContext);
}
argument.getVariableValues()
.importModuleValues(
this.importedModuleContexts.get(module.getNamespace()).getVariableValues()
this.importedModuleContexts.get(moduleLocation).getVariableValues()
);
argument.getInScopeSchemaTypes()
.importModuleTypes(
this.importedModuleContexts.get(module.getNamespace()).getInScopeSchemaTypes()
this.importedModuleContexts.get(moduleLocation).getInScopeSchemaTypes()
);
return argument;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,23 @@
import java.util.List;
import java.util.UUID;

import static org.rumbledb.expressions.module.Prolog.getFunctionDeclarationFromProlog;


public class FunctionInliningVisitor extends CloneVisitor {

private String queryLanguage;


private static FunctionDeclaration getDirectFunctionDeclaration(
Prolog prolog,
org.rumbledb.context.FunctionIdentifier functionIdentifier
) {
for (FunctionDeclaration declaration : prolog.getFunctionDeclarations()) {
if (declaration.getFunctionIdentifier().equals(functionIdentifier)) {
return declaration;
}
}
return null;
}

private boolean isVariableReferenced(Node expression, Name name) {
if (expression instanceof VariableReferenceExpression variableReference) {
return variableReference.getVariableName().equals(name);
Expand Down Expand Up @@ -465,7 +474,7 @@ public Node visitMainModule(MainModule mainModule, Node argument) {
// 2. Contain an exit statement.
@Override
public Node visitFunctionCall(FunctionCallExpression expression, Node argument) {
FunctionDeclaration targetFunction = getFunctionDeclarationFromProlog(
FunctionDeclaration targetFunction = getDirectFunctionDeclaration(
(Prolog) argument,
expression.getFunctionIdentifier()
);
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/org/rumbledb/compiler/ModuleImportLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/** Shared module import semantics for the JSONiq and XQuery frontends. */
final class ModuleImportLoader {

private ModuleImportLoader() {
}

public static LibraryModule load(
public static List<LibraryModule> load(
String namespace,
List<String> locationHints,
StaticContext importingModuleContext,
Expand All @@ -37,6 +40,7 @@ public static LibraryModule load(
String normalizedNamespace = URILiteralUtils.normalizeAsAnyURI(namespace);
List<String> candidates = locationHints.isEmpty() ? List.of(normalizedNamespace) : locationHints;
Exception lastFailure = null;
Map<String, LibraryModule> loadedModules = new LinkedHashMap<>();

for (String candidate : candidates) {
URI location;
Expand Down Expand Up @@ -64,12 +68,16 @@ public static LibraryModule load(
);
}

return module;
loadedModules.putIfAbsent(location.toString(), module);
} catch (IOException | CannotRetrieveResourceException e) {
lastFailure = e;
}
}

if (!loadedModules.isEmpty()) {
return new ArrayList<>(loadedModules.values());
}

RumbleException exception = new ModuleNotFoundException(
"Module not found: %s, cause: %s".formatted(
normalizedNamespace,
Expand Down
103 changes: 103 additions & 0 deletions src/main/java/org/rumbledb/compiler/ModuleImportSurface.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Authors: OpenAI
*
*/

package org.rumbledb.compiler;

import org.rumbledb.context.FunctionIdentifier;
import org.rumbledb.context.Name;
import org.rumbledb.exceptions.ExceptionMetadata;
import org.rumbledb.expressions.ExecutionMode;
import org.rumbledb.types.FunctionSignature;
import org.rumbledb.types.SequenceType;

import java.util.LinkedHashMap;
import java.util.Map;

/**
* The spec-visible surface of a library module import: only declarations made directly in the imported module.
*/
final class ModuleImportSurface {

static final class ImportedVariableBinding {
private final Name name;
private final SequenceType sequenceType;
private final ExceptionMetadata metadata;
private final ExecutionMode storageMode;
private final boolean assignable;

ImportedVariableBinding(
Name name,
SequenceType sequenceType,
ExceptionMetadata metadata,
ExecutionMode storageMode,
boolean assignable
) {
this.name = name;
this.sequenceType = sequenceType;
this.metadata = metadata;
this.storageMode = storageMode;
this.assignable = assignable;
}

public Name getName() {
return this.name;
}

public SequenceType getSequenceType() {
return this.sequenceType;
}

public ExceptionMetadata getMetadata() {
return this.metadata;
}

public ExecutionMode getStorageMode() {
return this.storageMode;
}

public boolean isAssignable() {
return this.assignable;
}
}

private final Map<Name, ImportedVariableBinding> variableBindings;
private final Map<FunctionIdentifier, FunctionSignature> functionSignatures;

ModuleImportSurface() {
this.variableBindings = new LinkedHashMap<>();
this.functionSignatures = new LinkedHashMap<>();
}

public void addVariableBinding(ImportedVariableBinding binding) {
this.variableBindings.put(binding.getName(), binding);
}

public void addFunctionSignature(FunctionIdentifier identifier, FunctionSignature signature) {
this.functionSignatures.put(identifier, signature);
}

public Map<Name, ImportedVariableBinding> getVariableBindings() {
return this.variableBindings;
}

public Map<FunctionIdentifier, FunctionSignature> getFunctionSignatures() {
return this.functionSignatures;
}
}
13 changes: 7 additions & 6 deletions src/main/java/org/rumbledb/compiler/ModulePruningVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@

package org.rumbledb.compiler;

import java.util.ArrayList;
import java.util.List;
import java.util.HashSet;
import java.util.Set;

import org.rumbledb.config.RumbleConfiguration;
import org.rumbledb.expressions.AbstractNodeVisitor;
Expand All @@ -38,7 +38,7 @@ public class ModulePruningVisitor extends AbstractNodeVisitor<Void> {

@SuppressWarnings("unused")
private final RumbleConfiguration configuration;
private final List<String> visitedModules;
private final Set<String> visitedModules;

/**
* Builds a new visitor.
Expand All @@ -47,17 +47,18 @@ public class ModulePruningVisitor extends AbstractNodeVisitor<Void> {
*/
ModulePruningVisitor(RumbleConfiguration configuration) {
this.configuration = configuration;
this.visitedModules = new ArrayList<>();
this.visitedModules = new HashSet<>();
}

@Override
public Void visitLibraryModule(LibraryModule libraryModule, Void argument) {
if (this.visitedModules.contains(libraryModule.getNamespace())) {
String moduleOrigin = libraryModule.getModuleIdentityOrLocation();
if (this.visitedModules.contains(moduleOrigin)) {
Prolog prolog = libraryModule.getProlog();
prolog.clearDeclarations();
}
visitDescendants(libraryModule, argument);
this.visitedModules.add(libraryModule.getNamespace());
this.visitedModules.add(moduleOrigin);
return argument;
}

Expand Down
Loading