Skip to content

Commit 8f98a75

Browse files
[ADD] XQuery: More optimizations on namespaced databases. #1763
1 parent a94537c commit 8f98a75

6 files changed

Lines changed: 116 additions & 17 deletions

File tree

basex-core/src/main/java/org/basex/data/Data.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,14 @@ public byte[] defaultNs() {
270270
return nspaces.defaultNs(meta.ndocs);
271271
}
272272

273+
/**
274+
* Checks if a default namespace is declared anywhere in the database.
275+
* @return result of check
276+
*/
277+
public boolean usesDefaultNs() {
278+
return nspaces.usesDefaultNs();
279+
}
280+
273281
// RETRIEVING VALUES ============================================================================
274282

275283
/**

basex-core/src/main/java/org/basex/data/Namespaces.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,31 @@ byte[] defaultNs(final int ndocs) {
146146
return uri(id);
147147
}
148148

149+
/**
150+
* Checks if a default namespace is declared anywhere in the database.
151+
* @return result of check
152+
*/
153+
public boolean usesDefaultNs() {
154+
return !isEmpty() && usesDefaultNs(root);
155+
}
156+
157+
/**
158+
* Recursively checks a namespace node and its descendants for a default namespace.
159+
* @param node namespace node
160+
* @return result of check
161+
*/
162+
private boolean usesDefaultNs(final NSNode node) {
163+
final int[] values = node.values();
164+
for(int v = 0; v < values.length; v += 2) {
165+
if(prefix(values[v]).length == 0 && uri(values[v + 1]).length != 0) return true;
166+
}
167+
final int ch = node.children();
168+
for(int c = 0; c < ch; c++) {
169+
if(usesDefaultNs(node.child(c))) return true;
170+
}
171+
return false;
172+
}
173+
149174
// Requesting Namespaces Based on Context =======================================================
150175

151176
/**

basex-core/src/main/java/org/basex/query/expr/path/NameTest.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,21 @@ public Test optimize(final Kind kn, final Data data) {
9191
// skip optimizations if data reference is not known at compile time
9292
if(data == null) return this;
9393

94-
// skip optimizations if more than one namespace is defined in the database
94+
// common default namespace (empty: none declared; null: ambiguous)
9595
final byte[] dataNs = data.defaultNs();
96-
if(dataNs == null) return this;
96+
if(dataNs == null) {
97+
// no default namespace anywhere: discard no-namespace name test if its local name is unknown
98+
// (the test is not reduced to local, as a prefixed name may share the same local name)
99+
if(scope.oneOf(Scope.FLEXIBLE, Scope.FULL) && !qname.hasURI() &&
100+
!kind.oneOf(Kind.GNODE, Kind.PROCESSING_INSTRUCTION) &&
101+
(kind == Kind.ATTRIBUTE || ns.length == 0) && !data.usesDefaultNs()) {
102+
final byte[] local = qname.local();
103+
final Names names = kind == Kind.ELEMENT ? data.elemNames : data.attrNames;
104+
if(!names.contains(local) && !names.contains(Token.concat(Token.XML, ':', local)))
105+
return null;
106+
}
107+
return this;
108+
}
97109

98110
// check if test may yield results
99111
if(scope.oneOf(Scope.FLEXIBLE, Scope.FULL) && !qname.hasURI()) {

basex-core/src/main/java/org/basex/query/util/index/IndexInfo.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,25 @@ public IndexType type(final Expr input, final IndexType type) {
8484
text = true;
8585
} else if(kind == Kind.ELEMENT) {
8686
// ensure that addressed elements only have text nodes as children
87-
// stop if database is unknown/out-dated, if namespaces occur, or if name test is not simple
88-
if(data == null || !data.meta.uptodate || !data.nspaces.isEmpty() ||
89-
!(last.test instanceof NameTest)) return null;
90-
87+
// stop if database is unknown/out-dated or if name test is not simple
88+
if(data == null || !data.meta.uptodate || !(last.test instanceof NameTest)) return null;
9189
test = (NameTest) last.test;
92-
if(test.name == null) return null;
9390

94-
final Stats stats = data.elemNames.stats(data.elemNames.index(test.name));
91+
// resolve local name for statistics lookup; sound only if its lexical name is unambiguous
92+
final byte[] local;
93+
if(data.nspaces.isEmpty()) {
94+
// no namespaces: one lexical name per local name
95+
if(test.name == null) return null;
96+
local = test.name;
97+
} else if(test.scope == NameTest.Scope.FULL && !test.qname.hasURI() &&
98+
!data.usesDefaultNs()) {
99+
// no default namespace: full no-namespace test maps to its no-prefix lexical name
100+
local = test.qname.local();
101+
} else {
102+
return null;
103+
}
104+
105+
final Stats stats = data.elemNames.stats(data.elemNames.index(local));
95106
if(stats == null || !stats.isLeaf()) return null;
96107
text = true;
97108
} else if(kind == Kind.ATTRIBUTE) {

basex-core/src/test/java/org/basex/query/index/IndexOptimizeTest.java

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -330,29 +330,69 @@ public final class IndexOptimizeTest extends SandboxTest {
330330
}
331331

332332
/**
333-
* Index access with namespaces.
333+
* Index access with namespaces (#1763).
334334
*/
335335
@Test public void gh1763() {
336336
final String doc = "<a xmlns:x=\"X\"><b>B</b></a>";
337337
execute(new CreateDB(NAME, doc));
338+
339+
// explicit text steps: rewritten for index access
338340
check("/a[.//text() = 'B']", doc, exists(ValueAccess.class));
339341
check("//a[.//text() = 'B']", doc, exists(ValueAccess.class));
340342
check("/a/b[text() = 'B']/..", doc, exists(ValueAccess.class));
341343
check("//b[text() = 'B']/..", doc, exists(ValueAccess.class));
342344
check("//*[text() = 'B']/..", doc, exists(ValueAccess.class));
343345

344-
// the following queries could be rewritten for index access
346+
// no default namespace is declared anywhere: no-namespace element tests are rewritten
347+
check("/a[b = 'B']", doc, exists(ValueAccess.class));
348+
check("//a[b = 'B']", doc, exists(ValueAccess.class));
349+
check("/*[b = 'B']", doc, exists(ValueAccess.class));
350+
check("//*[b = 'B']", doc, exists(ValueAccess.class));
351+
352+
// local-name wildcard tests are not rewritten (may match names in other namespaces)
345353
check("/a[*:b = 'B']", doc, empty(ValueAccess.class));
346354
check("//a[*:b = 'B']", doc, empty(ValueAccess.class));
347355
check("/*:a[*:b = 'B']", doc, empty(ValueAccess.class));
348356
check("//*:a[*:b = 'B']", doc, empty(ValueAccess.class));
349357
check("/*[*:b = 'B']", doc, empty(ValueAccess.class));
350358
check("//*[*:b = 'B']", doc, empty(ValueAccess.class));
351359

352-
check("/a[b = 'B']", doc, empty(ValueAccess.class));
353-
check("//a[b = 'B']", doc, empty(ValueAccess.class));
354-
check("/*[b = 'B']", doc, empty(ValueAccess.class));
355-
check("//*[b = 'B']", doc, empty(ValueAccess.class));
360+
// nonexistent no-namespace names: statically discarded
361+
check("/a/nonexistent", "", empty());
362+
check("//nonexistent[. = 'B']", "", empty());
363+
364+
// attribute values are rewritten for index access
365+
execute(new CreateDB(NAME, "<a xmlns:x=\"X\"><b id=\"7\">B</b></a>"));
366+
check("//b[@id = '7']", "<b xmlns:x=\"X\" id=\"7\">B</b>", exists(ValueAccess.class));
367+
}
368+
369+
/**
370+
* Index access with namespaces: soundness with prefixed same-local names (#1763).
371+
*/
372+
@Test public void gh1763Mixed() {
373+
// no-namespace <b> (leaf) and prefixed <x:b> (non-leaf) share the local name 'b'
374+
execute(new CreateDB(NAME, "<a xmlns:x=\"X\"><b>B</b><x:b><c>B</c></x:b></a>"));
375+
376+
// full no-namespace test is rewritten and matches only the no-namespace element
377+
check("count(/a[b = 'B'])", "1", exists(ValueAccess.class));
378+
check("count(//b[. = 'B'])", "1", exists(ValueAccess.class));
379+
// local-name wildcard is evaluated sequentially and matches both elements
380+
check("count(//*:b[. = 'B'])", "2", empty(ValueAccess.class));
381+
382+
// 'B' only occurs below the prefixed element: no-namespace test must not match
383+
execute(new CreateDB(NAME, "<a xmlns:x=\"X\"><b>b</b><x:b><c>B</c></x:b></a>"));
384+
check("count(/a[b = 'B'])", "0", exists(ValueAccess.class));
385+
}
386+
387+
/**
388+
* Index access on databases with a default namespace (#1763).
389+
*/
390+
@Test public void gh1763DefaultNs() {
391+
// single default namespace: a no-namespace element test never matches and is discarded
392+
execute(new CreateDB(NAME, "<a xmlns=\"Y\"><b>B</b></a>"));
393+
check("/a[b = 'B']", "", empty());
394+
// query default namespace matches the database default namespace: correct result
395+
check("declare default element namespace 'Y'; count(/a[b = 'B'])", "1");
356396
}
357397

358398
/** Index access for integer comparisons (issue #2069). */

basex-core/src/test/java/org/basex/query/index/ValueIndexTest.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ public void textIndex(final boolean mainmem) {
5656
execute(new CreateDB(NAME, FILE));
5757
check("count(//" + key + "[text() = " + value + "])",
5858
value.split(",").length, exists(ValueAccess.class));
59-
if(!key.equals("*")) check("//X[text() = 'unknown']", "", exists(DBNode.class));
59+
// existing but non-indexed element: sequential fallback (name 'b' is never a lookup key)
60+
if(!key.equals("*")) check("//b[text() = 'unknown']", "", exists(DBNode.class));
6061
});
6162
}
6263

@@ -73,7 +74,8 @@ public void attrIndex(final boolean mainmem) {
7374
execute(new CreateDB(NAME, FILE));
7475
check("count(//*[@" + key + " = " + value + "])",
7576
value.split(",").length, exists(ValueAccess.class));
76-
if(!key.equals("*")) check("//*[@x = 'unknown']", "", exists(DBNode.class));
77+
// existing but non-indexed attribute: sequential fallback (name 'b' is never a lookup key)
78+
if(!key.equals("*")) check("//*[@b = 'unknown']", "", exists(DBNode.class));
7779
});
7880
}
7981

@@ -88,7 +90,8 @@ public void attrIndex(final boolean mainmem) {
8890
check("count(//" + key + "[text() contains text { " + value + " }])",
8991
value.split(",").length,
9092
exists(FTIndexAccess.class));
91-
if(!key.equals("*")) check("//X[text() contains text 'unknown']", "", exists(DBNode.class));
93+
// existing but non-indexed element: sequential fallback (name 'b' is never a lookup key)
94+
if(!key.equals("*")) check("//b[text() contains text 'unknown']", "", exists(DBNode.class));
9295
});
9396
}
9497

0 commit comments

Comments
 (0)