Skip to content

Commit 8497da1

Browse files
authored
Merge pull request #5186 from nverwer/feature/sql-execute-result-namespace
[6.x.x] Allow setting the namespace on the result of sql:execute
2 parents d8785a8 + a410a22 commit 8497da1

File tree

2 files changed

+210
-35
lines changed

2 files changed

+210
-35
lines changed

extensions/modules/sql/src/main/java/org/exist/xquery/modules/sql/ExecuteFunction.java

Lines changed: 104 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,17 @@
2121
*/
2222
package org.exist.xquery.modules.sql;
2323

24-
import org.apache.logging.log4j.LogManager;
25-
import org.apache.logging.log4j.Logger;
26-
27-
import org.exist.dom.memtree.*;
28-
import org.exist.util.XMLReaderPool;
29-
import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
30-
import org.exist.xquery.*;
31-
import org.exist.xquery.value.*;
32-
import org.w3c.dom.Element;
33-
import org.w3c.dom.Node;
34-
import org.w3c.dom.NodeList;
35-
36-
import org.exist.Namespaces;
37-
import org.exist.dom.QName;
24+
import static java.nio.charset.StandardCharsets.UTF_8;
25+
import static org.exist.xquery.FunctionDSL.arities;
26+
import static org.exist.xquery.FunctionDSL.arity;
27+
import static org.exist.xquery.FunctionDSL.optParam;
28+
import static org.exist.xquery.FunctionDSL.param;
29+
import static org.exist.xquery.FunctionDSL.returnsOpt;
30+
import static org.exist.xquery.modules.sql.SQLModule.NAMESPACE_URI;
31+
import static org.exist.xquery.modules.sql.SQLModule.PREFIX;
3832

3933
import java.io.IOException;
4034
import java.io.PrintStream;
41-
4235
import java.io.Reader;
4336
import java.sql.Connection;
4437
import java.sql.PreparedStatement;
@@ -51,15 +44,38 @@
5144
import java.sql.Timestamp;
5245
import java.sql.Types;
5346

54-
import org.xml.sax.InputSource;
55-
import org.xml.sax.XMLReader;
56-
5747
import javax.annotation.Nullable;
5848

59-
import static java.nio.charset.StandardCharsets.UTF_8;
60-
import static org.exist.xquery.FunctionDSL.*;
61-
import static org.exist.xquery.modules.sql.SQLModule.NAMESPACE_URI;
62-
import static org.exist.xquery.modules.sql.SQLModule.PREFIX;
49+
import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
50+
import org.apache.logging.log4j.LogManager;
51+
import org.apache.logging.log4j.Logger;
52+
import org.exist.Namespaces;
53+
import org.exist.dom.QName;
54+
import org.exist.dom.memtree.AppendingSAXAdapter;
55+
import org.exist.dom.memtree.ElementImpl;
56+
import org.exist.dom.memtree.MemTreeBuilder;
57+
import org.exist.dom.memtree.ReferenceNode;
58+
import org.exist.dom.memtree.SAXAdapter;
59+
import org.exist.util.XMLReaderPool;
60+
import org.exist.xquery.BasicFunction;
61+
import org.exist.xquery.ErrorCodes;
62+
import org.exist.xquery.Expression;
63+
import org.exist.xquery.FunctionDSL;
64+
import org.exist.xquery.FunctionSignature;
65+
import org.exist.xquery.XPathException;
66+
import org.exist.xquery.XQueryContext;
67+
import org.exist.xquery.value.BooleanValue;
68+
import org.exist.xquery.value.DateTimeValue;
69+
import org.exist.xquery.value.FunctionParameterSequenceType;
70+
import org.exist.xquery.value.FunctionReturnSequenceType;
71+
import org.exist.xquery.value.IntegerValue;
72+
import org.exist.xquery.value.Sequence;
73+
import org.exist.xquery.value.Type;
74+
import org.w3c.dom.Element;
75+
import org.w3c.dom.Node;
76+
import org.w3c.dom.NodeList;
77+
import org.xml.sax.InputSource;
78+
import org.xml.sax.XMLReader;
6379

6480

6581
/**
@@ -80,11 +96,31 @@ public class ExecuteFunction extends BasicFunction {
8096
"connection-handle",
8197
Type.LONG,
8298
"The connection handle");
99+
private static final FunctionParameterSequenceType FS_PARAM_SQL_STATEMENT = param(
100+
"sql-statement",
101+
Type.STRING,
102+
"The SQL statement");
83103
private static final FunctionParameterSequenceType FS_PARAM_MAKE_NODE_FROM_COLUMN_NAME = param(
84104
"make-node-from-column-name",
85105
Type.BOOLEAN,
86106
"The flag that indicates whether the xml nodes should be formed from the column names" +
87107
" (in this mode a space in a Column Name will be replaced by an underscore!)");
108+
private static final FunctionParameterSequenceType FS_PARAM_STATEMENT_HANDLE = param(
109+
"statement-handle",
110+
Type.LONG,
111+
"The prepared statement handle");
112+
private static final FunctionParameterSequenceType FS_PARAM_PARAMETERS = optParam(
113+
"parameters",
114+
Type.ELEMENT,
115+
"Parameters for the prepared statement. e.g. <sql:parameters><sql:param sql:type=\"long\">1234</sql:param><sql:param sql:type=\"varchar\"><sql:null/></sql:param></sql:parameters>");
116+
private static final FunctionParameterSequenceType FS_PARAM_NAMESPACE_URI = param(
117+
"ns-uri",
118+
Type.STRING,
119+
"The uri of the result namespace.");
120+
private static final FunctionParameterSequenceType FS_PARAM_NAMESPACE_PREFIX = param(
121+
"ns-prefix",
122+
Type.STRING,
123+
"The prefix of the result namespace.");
88124

89125
static final FunctionSignature[] FS_EXECUTE = functionSignatures(
90126
FS_EXECUTE_NAME,
@@ -93,14 +129,29 @@ public class ExecuteFunction extends BasicFunction {
93129
arities(
94130
arity(
95131
FS_PARAM_CONNECTION_HANDLE,
96-
param("sql-statement", Type.STRING, "The SQL statement"),
132+
FS_PARAM_SQL_STATEMENT,
97133
FS_PARAM_MAKE_NODE_FROM_COLUMN_NAME
98134
),
99135
arity(
100136
FS_PARAM_CONNECTION_HANDLE,
101-
param("statement-handle", Type.LONG, "The prepared statement handle"),
102-
optParam("parameters", Type.ELEMENT, "Parameters for the prepared statement. e.g. <sql:parameters><sql:param sql:type=\"long\">1234</sql:param><sql:param sql:type=\"varchar\"><sql:null/></sql:param></sql:parameters>"),
137+
FS_PARAM_STATEMENT_HANDLE,
138+
FS_PARAM_PARAMETERS,
103139
FS_PARAM_MAKE_NODE_FROM_COLUMN_NAME
140+
),
141+
arity(
142+
FS_PARAM_CONNECTION_HANDLE,
143+
FS_PARAM_SQL_STATEMENT,
144+
FS_PARAM_MAKE_NODE_FROM_COLUMN_NAME,
145+
FS_PARAM_NAMESPACE_URI,
146+
FS_PARAM_NAMESPACE_PREFIX
147+
),
148+
arity(
149+
FS_PARAM_CONNECTION_HANDLE,
150+
FS_PARAM_STATEMENT_HANDLE,
151+
FS_PARAM_PARAMETERS,
152+
FS_PARAM_MAKE_NODE_FROM_COLUMN_NAME,
153+
FS_PARAM_NAMESPACE_URI,
154+
FS_PARAM_NAMESPACE_PREFIX
104155
)
105156
)
106157
);
@@ -147,20 +198,30 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
147198

148199
try {
149200
final boolean makeNodeFromColumnName;
201+
final String namespacePrefix;
202+
final String namespaceUri;
150203
final boolean executeResult;
151204

152205
// Static SQL or PreparedStatement?
153-
if (args.length == 3) {
206+
if (args.length == 3 || args.length == 5) {
154207

155208
// get the static SQL statement
156209
sql = args[1].getStringValue();
157210
stmt = con.createStatement();
158211
makeNodeFromColumnName = ((BooleanValue) args[2].itemAt(0)).effectiveBooleanValue();
212+
if (args.length == 5) {
213+
namespaceUri = args[3].itemAt(0).getStringValue();
214+
namespacePrefix = args[4].itemAt(0).getStringValue();
215+
} else {
216+
// The default namespace for result elements.
217+
namespaceUri = NAMESPACE_URI;
218+
namespacePrefix = PREFIX;
219+
}
159220

160221
//execute the static SQL statement
161222
executeResult = stmt.execute(sql);
162223

163-
} else if (args.length == 4) {
224+
} else if (args.length == 4 || args.length == 6) {
164225
//get the prepared statement
165226
final long statementUID = ((IntegerValue) args[1].itemAt(0)).getLong();
166227
final PreparedStatementWithSQL stmtWithSQL = SQLModule.retrievePreparedStatement(context, statementUID);
@@ -172,6 +233,14 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
172233
}
173234

174235
makeNodeFromColumnName = ((BooleanValue) args[3].itemAt(0)).effectiveBooleanValue();
236+
if (args.length == 6) {
237+
namespaceUri = args[4].itemAt(0).getStringValue();
238+
namespacePrefix = args[5].itemAt(0).getStringValue();
239+
} else {
240+
// The default namespace for result elements.
241+
namespaceUri = NAMESPACE_URI;
242+
namespacePrefix = PREFIX;
243+
}
175244

176245
if (!args[2].isEmpty()) {
177246
parametersElement = (Element) args[2].itemAt(0);
@@ -186,7 +255,7 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
186255
}
187256

188257
// return the XML result set
189-
return resultAsElement(makeNodeFromColumnName, executeResult, stmt, this);
258+
return resultAsElement(makeNodeFromColumnName, namespacePrefix, namespaceUri, executeResult, stmt, this);
190259

191260
} catch (final SQLException sqle) {
192261
LOG.error("sql:execute() Caught SQLException \"{}\" for SQL: \"{}\"", sqle.getMessage(), sql, sqle);
@@ -264,15 +333,15 @@ private void setParametersOnPreparedStatement(final Statement stmt, final Elemen
264333
}
265334
}
266335

267-
private ElementImpl resultAsElement(final boolean makeNodeFromColumnName,
336+
private ElementImpl resultAsElement(final boolean makeNodeFromColumnName, final String namespacePrefix, final String namespaceUri,
268337
final boolean executeResult, final Statement stmt, final Expression expression) throws SQLException, XPathException {
269338
context.pushDocumentContext();
270339
try {
271340
final MemTreeBuilder builder = context.getDocumentBuilder();
272341

273342
builder.startDocument();
274343

275-
builder.startElement(new QName("result", NAMESPACE_URI, PREFIX), null);
344+
builder.startElement(new QName("result", namespaceUri, namespacePrefix), null);
276345
builder.addAttribute(new QName("count", null, null), "-1");
277346
builder.addAttribute(new QName("updateCount", null, null), String.valueOf(stmt.getUpdateCount()));
278347

@@ -301,7 +370,7 @@ private ElementImpl resultAsElement(final boolean makeNodeFromColumnName,
301370
final int iColumns = rsmd.getColumnCount();
302371

303372
while (rs.next()) {
304-
builder.startElement(new QName("row", NAMESPACE_URI, PREFIX), null);
373+
builder.startElement(new QName("row", namespaceUri, namespacePrefix), null);
305374
builder.addAttribute(new QName("index", null, null), String.valueOf(rs.getRow()));
306375

307376
// get each tuple in the row
@@ -322,7 +391,7 @@ private ElementImpl resultAsElement(final boolean makeNodeFromColumnName,
322391
colElement = SQLUtils.escapeXmlAttr(columnName.replace(' ', '_'));
323392
}
324393

325-
builder.startElement(new QName(colElement, NAMESPACE_URI, PREFIX), null);
394+
builder.startElement(new QName(colElement, namespaceUri, namespacePrefix), null);
326395

327396
if (!makeNodeFromColumnName || columnName.length() <= 0) {
328397
final String name;
@@ -335,7 +404,7 @@ private ElementImpl resultAsElement(final boolean makeNodeFromColumnName,
335404
builder.addAttribute(new QName("name", null, null), name);
336405
}
337406

338-
builder.addAttribute(new QName(TYPE_ATTRIBUTE_NAME, NAMESPACE_URI, PREFIX), rsmd.getColumnTypeName(i + 1));
407+
builder.addAttribute(new QName(TYPE_ATTRIBUTE_NAME, namespaceUri, namespacePrefix), rsmd.getColumnTypeName(i + 1));
339408
builder.addAttribute(new QName(TYPE_ATTRIBUTE_NAME, Namespaces.SCHEMA_NS, "xs"), Type.getTypeName(SQLUtils.sqlTypeToXMLType(rsmd.getColumnType(i + 1))));
340409

341410
//get the content
@@ -346,7 +415,7 @@ private ElementImpl resultAsElement(final boolean makeNodeFromColumnName,
346415

347416
if (rs.wasNull()) {
348417
// Add a null indicator attribute if the value was SQL Null
349-
builder.addAttribute(new QName("null", NAMESPACE_URI, PREFIX), "true");
418+
builder.addAttribute(new QName("null", namespaceUri, namespacePrefix), "true");
350419
} else {
351420
try (final Reader charStream = sqlXml.getCharacterStream()) {
352421
final InputSource src = new InputSource(charStream);
@@ -375,7 +444,7 @@ private ElementImpl resultAsElement(final boolean makeNodeFromColumnName,
375444

376445
if (rs.wasNull()) {
377446
// Add a null indicator attribute if the value was SQL Null
378-
builder.addAttribute(new QName("null", NAMESPACE_URI, PREFIX), "true");
447+
builder.addAttribute(new QName("null", namespaceUri, namespacePrefix), "true");
379448
} else {
380449
if (colValue != null) {
381450
builder.characters(colValue);
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* eXist-db Open Source Native XML Database
3+
* Copyright (C) 2001 The eXist-db Authors
4+
*
5+
6+
* http://www.exist-db.org
7+
*
8+
* This library is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU Lesser General Public
10+
* License as published by the Free Software Foundation; either
11+
* version 2.1 of the License, or (at your option) any later version.
12+
*
13+
* This library is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
* Lesser General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Lesser General Public
19+
* License along with this library; if not, write to the Free Software
20+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21+
*/
22+
package org.exist.xquery.modules.sql;
23+
24+
import com.evolvedbinary.j8fu.tuple.Tuple2;
25+
import org.exist.EXistException;
26+
import org.exist.dom.memtree.ElementImpl;
27+
import org.exist.security.PermissionDeniedException;
28+
import org.exist.source.Source;
29+
import org.exist.source.StringSource;
30+
import org.exist.storage.BrokerPool;
31+
import org.exist.storage.DBBroker;
32+
import org.exist.storage.txn.Txn;
33+
import org.exist.test.ExistEmbeddedServer;
34+
import org.exist.xquery.XPathException;
35+
import org.exist.xquery.XQueryContext;
36+
import org.exist.xquery.value.Sequence;
37+
import org.exist.xquery.value.Type;
38+
import org.junit.Rule;
39+
import org.junit.Test;
40+
import org.w3c.dom.Element;
41+
42+
import java.io.IOException;
43+
44+
import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple;
45+
import static org.exist.xquery.modules.sql.Util.executeQuery;
46+
import static org.exist.xquery.modules.sql.Util.withCompiledQuery;
47+
import static org.junit.Assert.assertEquals;
48+
import static org.junit.Assert.assertTrue;
49+
50+
/**
51+
* SQL Execute Integration Tests.
52+
*
53+
* @author <a href="mailto:[email protected]">Adam Retter</a>
54+
*/
55+
public class ExecuteIT {
56+
57+
@Rule
58+
public final ExistEmbeddedServer existEmbeddedServer = new ExistEmbeddedServer(true, true);
59+
60+
@Rule
61+
public final H2DatabaseResource h2Database = new H2DatabaseResource();
62+
63+
@Test
64+
public void executeResultsInSqlNS() throws EXistException, XPathException, PermissionDeniedException, IOException {
65+
executeForNS(SQLModule.NAMESPACE_URI, SQLModule.PREFIX);
66+
}
67+
68+
@Test
69+
public void executeResultsInCustomNS() throws EXistException, XPathException, PermissionDeniedException, IOException {
70+
executeForNS("http://custom/ns", "custom");
71+
}
72+
73+
private void executeForNS(final String namespace, final String prefix) throws EXistException, XPathException, PermissionDeniedException, IOException {
74+
final String mainQuery =
75+
"import module namespace sql = \"http://exist-db.org/xquery/sql\";\n" +
76+
"let $conn := sql:get-connection(\"" + h2Database.getDriverClass().getName() + "\", \"" + h2Database.getUrl() + "\", \"" + h2Database.getUser() + "\", \"" + h2Database.getPassword() + "\")\n" +
77+
"return\n" +
78+
" sql:execute($conn, \"SELECT 'Hello World' FROM DUAL;\", true(), \"" + namespace + "\", \"" + prefix + "\")";
79+
80+
final BrokerPool pool = existEmbeddedServer.getBrokerPool();
81+
final Source mainQuerySource = new StringSource(mainQuery);
82+
try (final DBBroker broker = pool.getBroker();
83+
final Txn transaction = pool.getTransactionManager().beginTransaction()) {
84+
85+
final Tuple2<String, String> namespaceAndPrefix = withCompiledQuery(broker, mainQuerySource, mainCompiledQuery -> {
86+
final XQueryContext mainQueryContext = mainCompiledQuery.getContext();
87+
88+
// execute the query
89+
final Sequence result = executeQuery(broker, mainCompiledQuery);
90+
91+
// check that the namespace of the result element is in the 'sql' namespace
92+
assertEquals(1, result.getItemCount());
93+
assertTrue(result.itemAt(0) instanceof Element);
94+
assertEquals(Type.ELEMENT, result.itemAt(0).getType());
95+
final Element element = (ElementImpl) result.itemAt(0);
96+
97+
return Tuple(element.getNamespaceURI(), element.getPrefix());
98+
});
99+
100+
assertEquals(namespace, namespaceAndPrefix._1);
101+
assertEquals(prefix, namespaceAndPrefix._2);
102+
103+
transaction.commit();
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)