[jOOQ/jOOQ#19233] SQLDataType.XML should be bound as XMLTYPE, not as

String in Oracle
This commit is contained in:
Lukas Eder 2025-10-20 13:43:10 +02:00
parent 16cd10a484
commit eaa1d8c32d
6 changed files with 363 additions and 24 deletions

View File

@ -546,7 +546,9 @@ public final class XMLFormat {
ABSENT_ELEMENT,
/**
* A <code>null</code> value is represented by a <code>xsi:nil="true"</code> attribute.
* A <code>null</code> value is represented by a
* <code>xsi:nil="true"</code> attribute if {@link XMLFormat#xmlns()} is
* set, or <code>nil="true"</code>, if it is not set.
*/
XSI_NIL
}

View File

@ -123,6 +123,7 @@ import org.xml.sax.helpers.DefaultHandler;
*/
abstract class AbstractResult<R extends Record> extends AbstractFormattable implements FieldsTrait, Iterable<R> {
private static final String XSI_SCHEMA = "http://www.w3.org/2001/XMLSchema-instance";
final AbstractRow<R> fields;
AbstractResult(Configuration configuration, AbstractRow<R> row) {
@ -804,7 +805,11 @@ abstract class AbstractResult<R extends Record> extends AbstractFormattable impl
if (format.xmlns()) {
format = format.xmlns(false);
writer.append(" xmlns=\"" + Constants.NS_EXPORT + "\"");
if (format.nullFormat() == XSI_NIL)
writer.append(" xsi:xmlns=\"" + XSI_SCHEMA + "\"");
}
writer.append(">");
if (format.header()) {
@ -878,6 +883,9 @@ abstract class AbstractResult<R extends Record> extends AbstractFormattable impl
if (format.xmlns()) {
format = format.xmlns(false);
writer.append(" xmlns=\"" + Constants.NS_EXPORT + "\"");
if (format.nullFormat() == XSI_NIL)
writer.append(" xsi:xmlns=\"" + XSI_SCHEMA + "\"");
}
if (record == null) {
@ -906,7 +914,7 @@ abstract class AbstractResult<R extends Record> extends AbstractFormattable impl
if (value == null) {
if (format.nullFormat() == XSI_NIL)
writer.append(" xsi:nil=\"true\"");
writer.append(" ").append(nil(format)).append("=\"true\"");
writer.append("/>");
}
@ -943,7 +951,7 @@ abstract class AbstractResult<R extends Record> extends AbstractFormattable impl
if (o == null) {
if (format.nullFormat() == XSI_NIL)
writer.append(" xsi:nil=\"true\"");
writer.append(" ").append(nil(format)).append("=\"true\"");
writer.append("/>");
}
@ -1233,11 +1241,12 @@ abstract class AbstractResult<R extends Record> extends AbstractFormattable impl
Element eResult = document.createElement("result");
if (format.xmlns())
if (format.xmlns()) {
eResult.setAttribute("xmlns", Constants.NS_EXPORT);
if (format.nullFormat() == XSI_NIL)
eResult.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
eResult.setAttribute("xmlns:xsi", XSI_SCHEMA);
}
document.appendChild(eResult);
@ -1318,7 +1327,7 @@ abstract class AbstractResult<R extends Record> extends AbstractFormattable impl
) {
if (value == null) {
if (format.nullFormat() == XSI_NIL)
eParent.setAttribute("xsi:nil", "true");
eParent.setAttribute(nil(format), "true");
}
else if (value instanceof Formattable f) {
Document d = f.intoXML(format);
@ -1338,7 +1347,7 @@ abstract class AbstractResult<R extends Record> extends AbstractFormattable impl
if (o == null) {
if (format.nullFormat() == XSI_NIL)
eElement.setAttribute("xsi:nil", "true");
eElement.setAttribute(nil(format), "true");
}
else
intoXMLContent(format, builder, document, o, eElement);
@ -1357,7 +1366,11 @@ abstract class AbstractResult<R extends Record> extends AbstractFormattable impl
eParent.setTextContent(format0(value, false, false));
}
private final Node childElement(Node n) {
private static final String nil(XMLFormat format) {
return format.xmlns() ? "xsi:nil" : "nil";
}
private static final Node childElement(Node n) {
NodeList l = n.getChildNodes();
for (int i = 0; i < l.getLength(); i++) {
@ -1431,9 +1444,13 @@ abstract class AbstractResult<R extends Record> extends AbstractFormattable impl
handler.startDocument();
if (format.xmlns())
if (format.xmlns()) {
handler.startPrefixMapping("", Constants.NS_EXPORT);
if (format.nullFormat() == XSI_NIL)
handler.startPrefixMapping("xsi", XSI_SCHEMA);
}
handler.startElement("", "", "result", empty);
if (format.header()) {
handler.startElement("", "", "fields", empty);
@ -1501,8 +1518,12 @@ abstract class AbstractResult<R extends Record> extends AbstractFormattable impl
if (format.header())
handler.endElement("", "", "records");
if (format.xmlns())
if (format.xmlns()) {
if (format.nullFormat() == XSI_NIL)
handler.endPrefixMapping("xsi");
handler.endPrefixMapping("");
}
handler.endDocument();
return handler;
@ -1564,7 +1585,7 @@ abstract class AbstractResult<R extends Record> extends AbstractFormattable impl
return formatted;
}
private static final String escapeXML(String string) {
static final String escapeXML(String string) {
return StringUtils.replaceEach(string,
new String[] { "\"", "'", "<", ">", "&" },
new String[] { "&quot;", "&apos;", "&lt;", "&gt;", "&amp;"});

View File

@ -107,6 +107,7 @@ import static org.jooq.impl.DSL.inline;
import static org.jooq.impl.DSL.log;
import static org.jooq.impl.DSL.name;
import static org.jooq.impl.DSL.using;
import static org.jooq.impl.DSL.xmlserializeContent;
import static org.jooq.impl.DefaultBinding.DefaultDoubleBinding.REQUIRES_LITERAL_CAST;
import static org.jooq.impl.DefaultBinding.DefaultDoubleBinding.infinity;
import static org.jooq.impl.DefaultBinding.DefaultDoubleBinding.nan;
@ -146,11 +147,14 @@ import static org.jooq.impl.Keywords.K_TRUE;
import static org.jooq.impl.Keywords.K_YEAR_TO_DAY;
import static org.jooq.impl.Keywords.K_YEAR_TO_FRACTION;
import static org.jooq.impl.Names.N_BYTEA;
import static org.jooq.impl.Names.N_CREATEXML;
import static org.jooq.impl.Names.N_HEX;
import static org.jooq.impl.Names.N_JSON_PARSE;
import static org.jooq.impl.Names.N_PARSE_JSON;
import static org.jooq.impl.Names.N_ST_GEOMFROMTEXT;
import static org.jooq.impl.Names.N_ST_GEOMFROMWKB;
import static org.jooq.impl.Names.N_TO_BINARY;
import static org.jooq.impl.Names.N_XMLTYPE;
import static org.jooq.impl.R2DBC.isR2dbc;
import static org.jooq.impl.SQLDataType.BIGINT;
import static org.jooq.impl.SQLDataType.BLOB;
@ -333,6 +337,8 @@ import org.jooq.types.YearToMonth;
import org.jooq.types.YearToSecond;
import org.jooq.util.postgres.PostgresUtils;
import org.jetbrains.annotations.Nullable;
// ...
// ...
@ -934,6 +940,15 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
}
}
if (dataType.isUUID()) {
switch (ctx.family()) {
@ -1220,7 +1235,7 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
private final void sql(BindingSQLContext<U> ctx, T value) throws SQLException {
if (ctx.render().paramType() == INLINED)
if (value == null)
ctx.render().visit(K_NULL);
sqlInlineNull0(ctx);
else
sqlInline0(ctx, value);
else
@ -1330,7 +1345,10 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
ctx.statement().registerOutParameter(ctx.index(), sqltype(ctx.statement(), ctx.configuration()));
}
@SuppressWarnings("unused")
/* non-final */ void sqlInlineNull0(BindingSQLContext<U> ctx) {
ctx.render().visit(K_NULL);
}
/* non-final */ void sqlInline0(BindingSQLContext<U> ctx, T value) throws SQLException {
sqlInline1(ctx, value);
}
@ -6554,6 +6572,54 @@ public class DefaultBinding<T, U> implements Binding<T, U> {
super(dataType, converter);
}
@Override
final void sqlInlineNull0(BindingSQLContext<U> ctx) {
switch (ctx.family()) {
default:
super.sqlInlineNull0(ctx);
break;
}
}
@Override
final void sqlInline0(BindingSQLContext<U> ctx, XML value) throws SQLException {
switch (ctx.family()) {
default:
super.sqlInline0(ctx, value);
break;
}
}
@Override
final void sqlBind0(BindingSQLContext<U> ctx, XML value) throws SQLException {
switch (ctx.family()) {
default:
super.sqlBind0(ctx, value);
break;
}
}
@Override
final void setNull0(BindingSetStatementContext<U> ctx) throws SQLException {

View File

@ -628,6 +628,12 @@ final class Multiset<R extends Record> extends AbstractField<Result<R>> implemen

View File

@ -97,6 +97,7 @@ final class Names {
static final Name N_COUNT_IF = systemName("count_if");
static final Name N_covarPop = systemName("covarPop");
static final Name N_covarSamp = systemName("covarSamp");
static final Name N_CREATEXML = systemName("createxml");
static final Name N_CUBE = systemName("cube");
static final Name N_CURRENT_BIGDATETIME = systemName("current_bigdatetime");
static final Name N_CURRENT_DATE = systemName("current_date");

View File

@ -38,6 +38,8 @@
package org.jooq.impl;
// ...
import static org.jooq.XML.xml;
import static org.jooq.impl.AbstractResult.escapeXML;
import static org.jooq.impl.DSL.field;
import static org.jooq.impl.DSL.name;
import static org.jooq.impl.DefaultDataType.getDataType;
@ -51,18 +53,21 @@ import static org.jooq.impl.Tools.row0;
import static org.jooq.tools.StringUtils.defaultIfBlank;
import java.io.ByteArrayInputStream;
import java.io.StringWriter;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.jooq.ContextConverter;
import org.jooq.Converter;
import org.jooq.ConverterContext;
import org.jooq.DSLContext;
import org.jooq.DataType;
@ -71,22 +76,181 @@ import org.jooq.Record;
import org.jooq.Result;
import org.jooq.exception.DataAccessException;
import org.jooq.tools.JooqLogger;
import org.jooq.tools.StringUtils;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.DefaultHandler;
/**
* @author Lukas Eder
*/
final class XMLHandler<R extends Record> extends DefaultHandler {
final class XMLHandler<R extends Record>
extends
DefaultHandler
implements
LexicalHandler
{
private static final JooqLogger log = JooqLogger.getLogger(XMLHandler.class);
private static final boolean debug = false;
private final DSLContext ctx;
private final Deque<State<R>> states;
private State<R> s;
static class XMLWriter extends DefaultHandler implements LexicalHandler {
final StringWriter out;
int level;
String lastElement;
String[] lastAttributes;
boolean cdata;
XMLWriter() {
out = new StringWriter();
// [#19229] TODO: StringWriter seems good enough for our test cases. Perhaps, switch to XMLStreamWriter, instead?
}
private boolean flushLastElement(boolean end) {
if (lastElement != null) {
out.write('<');
out.write(lastElement);
if (lastAttributes != null) {
for (int i = 0; i < lastAttributes.length; i += 2) {
out.write(' ');
out.write(lastAttributes[i]);
out.write("=\"");
out.write(escapeXML(lastAttributes[i + 1]));
out.write("\"");
}
}
if (end)
out.write("/>");
else
out.write('>');
lastElement = null;
lastAttributes = null;
return true;
}
else
return false;
}
// --------------------------------------------------------------------
// ContentHandler API
// --------------------------------------------------------------------
@Override
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
level++;
flushLastElement(false);
lastElement = qName;
// [#19229] Attributes is a mutable object in some parsers (e.g. ojdbc ships its own),
// so we have to copy its contents
if (atts != null && atts.getLength() > 0) {
lastAttributes = new String[atts.getLength() * 2];
for (int i = 0; i < atts.getLength(); i++) {
lastAttributes[i * 2] = atts.getQName(i);
lastAttributes[i * 2 + 1] = atts.getValue(i);
}
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if (!flushLastElement(true)) {
out.write("</");
out.write(qName);
out.write('>');
}
level--;
}
@Override
public void processingInstruction(String target, String data) throws SAXException {
flushLastElement(false);
out.write("<?");
out.write(target);
if (!StringUtils.isEmpty(data)) {
out.write(' ');
out.write(data);
}
out.write("?>");
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
flushLastElement(false);
if (cdata)
out.write(ch, start, length);
else
out.write(escapeXML(new String(ch, start, length)));
}
@Override
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
flushLastElement(false);
out.write(ch, start, length);
}
// --------------------------------------------------------------------
// LexicalHandler API
// --------------------------------------------------------------------
@Override
public void startCDATA() throws SAXException {
cdata = true;
flushLastElement(false);
out.write("<![CDATA[");
}
@Override
public void endCDATA() throws SAXException {
flushLastElement(false);
out.write("]]>");
cdata = false;
}
@Override
public void comment(char[] ch, int start, int length) throws SAXException {
flushLastElement(false);
out.write("<!--");
out.write(ch, start, length);
out.write("-->");
}
// [#19229] TODO: Implement these if needed
@Override
public void startDTD(String name, String publicId, String systemId) throws SAXException {
}
@Override
public void endDTD() throws SAXException {
}
@Override
public void startEntity(String name) throws SAXException {
}
@Override
public void endEntity(String name) throws SAXException {
}
}
private static class State<R extends Record> {
final DSLContext ctx;
AbstractRow<R> row;
@ -101,6 +265,7 @@ final class XMLHandler<R extends Record> extends DefaultHandler {
final List<Object> values;
List<Object> elements;
int column;
XMLWriter writer;
@SuppressWarnings("unchecked")
State(DSLContext ctx, AbstractRow<R> row, Class<? extends R> recordType) {
@ -187,6 +352,11 @@ final class XMLHandler<R extends Record> extends DefaultHandler {
SAXParser saxParser = factory.newSAXParser();
// TODO: Why does the SAXParser replace \r by \n?
try {
saxParser.setProperty("http://xml.org/sax/properties/lexical-handler", this);
}
catch (SAXNotRecognizedException | SAXNotSupportedException ignore) {}
saxParser.parse(new ByteArrayInputStream(string.getBytes(ctx.configuration().charsetProvider().provide())), this);
return s.result;
}
@ -202,7 +372,10 @@ final class XMLHandler<R extends Record> extends DefaultHandler {
if (log.isDebugEnabled())
log.debug("> " + qName);
if (!s.inResult && "result".equalsIgnoreCase(qName)) {
if (s.writer != null) {
s.writer.startElement(uri, localName, qName, attributes);
}
else if (!s.inResult && "result".equalsIgnoreCase(qName)) {
s.inResult = true;
}
else if (s.inColumn && "result".equalsIgnoreCase(qName)) {
@ -265,15 +438,23 @@ final class XMLHandler<R extends Record> extends DefaultHandler {
s.inColumn = true;
DataType<?> t = s.fields.get(s.column).getDataType();
Field<?> f = s.fields.get(s.column);
DataType<?> t = f.getDataType();
// [#13181] String NULL and '' values cannot be distinguished without xsi:nil
if (t.isString() && !isNil(attributes))
if (t.isString() && !isNil(attributes)) {
s.values.add("");
else if (t.isArray() && !isNil(attributes))
}
else if (t.isArray() && !isNil(attributes)) {
s.elements = new ArrayList<>();
else if (!t.isMultiset() && !t.isRecord())
}
else if (!t.isMultiset() && !t.isRecord()) {
s.values.add(null);
// [#19229] Copy XML content
if (f.getDataType().isXML() && s.writer == null)
s.writer = new XMLWriter();
}
}
}
@ -291,7 +472,18 @@ final class XMLHandler<R extends Record> extends DefaultHandler {
if (log.isDebugEnabled())
log.debug("< " + qName);
if (states.isEmpty() && s.inResult && s.inRecord == 0 && "result".equalsIgnoreCase(qName)) {
if (s.writer != null && s.writer.level == 0)
s.writer = null;
if (s.writer != null) {
s.writer.endElement(uri, localName, qName);
if (s.writer.level == 0) {
s.values.set(s.values.size() - 1, xml(s.writer.out.toString()));
s.writer = null;
}
}
else if (states.isEmpty() && s.inResult && s.inRecord == 0 && "result".equalsIgnoreCase(qName)) {
if (s.result == null)
initResult();
@ -367,11 +559,20 @@ final class XMLHandler<R extends Record> extends DefaultHandler {
return allMatch(fields, f -> "value".equalsIgnoreCase(f.getName()));
}
@Override
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
if (s.writer != null)
s.writer.ignorableWhitespace(ch, start, length);
}
@Override
public final void characters(char[] ch, int start, int length) throws SAXException {
DataType<?> t;
if (s.inColumn
if (s.writer != null) {
s.writer.characters(ch, start, length);
}
else if (s.inColumn
&& !(t = s.fields.get(s.column).getDataType()).isRecord()
&& !t.isMultiset()
&& (!t.isArray() || s.inElement)
@ -390,4 +591,46 @@ final class XMLHandler<R extends Record> extends DefaultHandler {
s.values.set(s.column, old + value);
}
}
@Override
public void startDTD(String name, String publicId, String systemId) throws SAXException {
if (s != null && s.writer != null)
s.writer.startDTD(name, publicId, systemId);
}
@Override
public void endDTD() throws SAXException {
if (s != null && s.writer != null)
s.writer.endDTD();
}
@Override
public void startEntity(String name) throws SAXException {
if (s != null && s.writer != null)
s.writer.startEntity(name);
}
@Override
public void endEntity(String name) throws SAXException {
if (s != null && s.writer != null)
s.writer.endEntity(name);
}
@Override
public void startCDATA() throws SAXException {
if (s != null && s.writer != null)
s.writer.startCDATA();
}
@Override
public void endCDATA() throws SAXException {
if (s != null && s.writer != null)
s.writer.endCDATA();
}
@Override
public void comment(char[] ch, int start, int length) throws SAXException {
if (s != null && s.writer != null)
s.writer.comment(ch, start, length);
}
}