Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions vertx-mssql-client/src/main/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ Currently the client supports the following SQL Server types:
| `GUID`
| `java.util.UUID`
|

| `json`
| `io.vertx.core.json.JsonObject` / `io.vertx.core.json.JsonArray`
| Native JSON type (SQL Server 2025+). See <<handling-json>>
|===

Tuple decoding uses the above types when storing values.
Expand Down Expand Up @@ -290,6 +294,31 @@ Otherwise, you should declare the type explicitly using one of the {@link io.ver
{@link examples.MSSQLClientExamples#explicitNullHandling}
----

[[handling-json]]
=== Handling JSON

SQL Server 2025 introduces a native `json` data type for storing JSON documents.

IMPORTANT: The native `json` type requires SQL Server 2025 or later and only supports JSON objects and arrays.
Scalars, booleans, and the JSON null literal are not supported.

The `json` data type is represented by the following Java types:

- `io.vertx.core.json.JsonObject`
- `io.vertx.core.json.JsonArray`

[source,$lang]
----
{@link examples.MSSQLClientExamples#jsonExample(io.vertx.sqlclient.SqlClient)}
----

[source,$lang]
----
{@link examples.MSSQLClientExamples#jsonArrayExample(io.vertx.sqlclient.SqlClient)}
----

NOTE: Attempting to insert scalar values, booleans, or {@link io.vertx.sqlclient.Tuple#JSON_NULL} will result in an error.

== Collector queries

You can use Java collectors with the query API:
Expand Down
40 changes: 40 additions & 0 deletions vertx-mssql-client/src/main/java/examples/MSSQLClientExamples.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
package examples;

import io.vertx.core.Vertx;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.ClientSSLOptions;
import io.vertx.core.net.PemTrustOptions;
import io.vertx.docgen.Source;
Expand Down Expand Up @@ -353,4 +355,42 @@ public void infoHandler(MSSQLConnection connection) {
System.out.println("Received info " + info.getSeverity() + "" + info.getMessage());
});
}

public void jsonExample(SqlClient client) {
JsonObject json = new JsonObject()
.put("name", "Alice")
.put("age", 30);

client
.preparedQuery("INSERT INTO users (id, data) VALUES (@p1, @p2)")
.execute(Tuple.of(1, json))
.compose(v -> client
.preparedQuery("SELECT data FROM users WHERE id = @p1")
.execute(Tuple.of(1)))
.onComplete(ar -> {
if (ar.succeeded()) {
Row row = ar.result().iterator().next();
JsonObject data = row.getJsonObject(0);
System.out.println("Received: " + data.getString("name"));
}
});
}

public void jsonArrayExample(SqlClient client) {
JsonArray tags = new JsonArray().add("admin").add("user");

client
.preparedQuery("INSERT INTO users (id, data) VALUES (@p1, @p2)")
.execute(Tuple.of(2, tags))
.compose(v -> client
.preparedQuery("SELECT data FROM users WHERE id = @p1")
.execute(Tuple.of(2)))
.onComplete(ar -> {
if (ar.succeeded()) {
Row row = ar.result().iterator().next();
JsonArray data = row.getJsonArray(0);
System.out.println("Received: " + data);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2011-2021 Contributors to the Eclipse Foundation
* Copyright (c) 2011-2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
Expand All @@ -12,6 +12,8 @@
package io.vertx.mssqlclient.impl;

import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.sqlclient.impl.RowBase;
import io.vertx.sqlclient.internal.RowDescriptorBase;

Expand Down Expand Up @@ -59,6 +61,10 @@ public <T> T get(Class<T> type, int position) {
return type.cast(getOffsetDateTime(position));
} else if (type == Buffer.class) {
return type.cast(getBuffer(position));
} else if (type == JsonObject.class) {
return type.cast(getJsonObject(position));
} else if (type == JsonArray.class) {
return type.cast(getJsonArray(position));
} else if (type == UUID.class) {
return type.cast(getValue(position));
} else if (type == Object.class) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2011-2022 Contributors to the Eclipse Foundation
* Copyright (c) 2011-2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
Expand All @@ -18,6 +18,9 @@
import io.netty.util.collection.IntObjectMap;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.internal.buffer.BufferInternal;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;

import java.math.BigDecimal;
import java.math.BigInteger;
Expand Down Expand Up @@ -969,6 +972,40 @@ public Object decodeValue(ByteBuf byteBuf, TypeInfo typeInfo) {
return result;
}
},
JSON(0xF4) {
@Override
public TypeInfo decodeTypeInfo(ByteBuf byteBuf) {
return new TypeInfo().maxLength(0xFFFF);
}

@Override
public JDBCType jdbcType(TypeInfo typeInfo) {
return JDBCType.OTHER;
}

@Override
public Object decodeValue(ByteBuf byteBuf, TypeInfo typeInfo) {
long payloadLength = byteBuf.readLongLE();
if (isPLPNull(payloadLength)) {
return null;
}
String jsonString = readPLP(byteBuf).toString(StandardCharsets.UTF_16LE);
return Json.decodeValue(jsonString);
}

@Override
public String paramDefinition(Object value) {
// SQL Server expects text for JSON params
return NVARCHAR.paramDefinition(value);
}

@Override
public void encodeParam(ByteBuf byteBuf, String name, boolean out, Object value) {
// SQL Server expects text for JSON params
NVARCHAR.encodeParam(byteBuf, name, out, value);
}
},

SSVARIANT(0x62) {
@Override
public TypeInfo decodeTypeInfo(ByteBuf byteBuf) {
Expand Down Expand Up @@ -1108,6 +1145,8 @@ public void encodeParam(ByteBuf byteBuf, String name, boolean out, Object value)
typesByValueClass.put(OffsetDateTime.class, DATETIMEOFFSETN);
typesByValueClass.put(UUID.class, GUID);
typesByValueClass.put(Buffer.class, BIGVARBINARY);
typesByValueClass.put(JsonObject.class, JSON);
typesByValueClass.put(JsonArray.class, JSON);
}

public static DataType forId(int id) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2011-2021 Contributors to the Eclipse Foundation
* Copyright (c) 2011-2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
Expand Down Expand Up @@ -56,7 +56,7 @@ void encode() {
LoginPacket.OPTION_FLAGS2_ODBC_ON
); // OptionFlags2
content.writeByte(LoginPacket.DEFAULT_TYPE_FLAGS); // TypeFlags
content.writeByte(LoginPacket.DEFAULT_OPTION_FLAGS3); // OptionFlags3
content.writeByte(LoginPacket.DEFAULT_OPTION_FLAGS3 | LoginPacket.OPTION_FLAGS3_EXTENSION); // OptionFlags3
content.writeZero(8); // ClientTimeZone + ClientLCID

/*
Expand Down Expand Up @@ -98,9 +98,10 @@ void encode() {
content.writeZero(2); // offset
content.writeShortLE(serverName.length());

// Unused or Extension
int unusedOffsetLengthIdx = content.writerIndex();
content.writeZero(4);
// Extension
int extensionOffsetLengthIdx = content.writerIndex();
content.writeZero(2); // offset
content.writeShortLE(4);

// CltIntName
CharSequence interfaceLibraryName = properties.get("clientInterfaceName");
Expand Down Expand Up @@ -158,7 +159,9 @@ void encode() {
content.setShortLE(serverNameOffsetLengthIdx, content.writerIndex() - startIdx);
content.writeCharSequence(serverName, UTF_16LE);

content.setShortLE(unusedOffsetLengthIdx, content.writerIndex() - startIdx);
content.setShortLE(extensionOffsetLengthIdx, content.writerIndex() - startIdx);
int featureExtOffsetIdx = content.writerIndex();
content.writeIntLE(0);

content.setShortLE(cltIntNameOffsetLengthIdx, content.writerIndex() - startIdx);
content.writeCharSequence(interfaceLibraryName, UTF_16LE);
Expand All @@ -174,6 +177,14 @@ void encode() {

content.setShortLE(changePasswordOffsetLengthIdx, content.writerIndex() - startIdx);

content.setIntLE(featureExtOffsetIdx, content.writerIndex() - startIdx);
// TDS_FEATURE_EXT_JSONSUPPORT
content.writeByte(0x0D); // feature ID
content.writeIntLE(1); // data length
content.writeByte(0x01); // JSON support version 1
// FEATUREEXT terminator
content.writeByte(0xFF);

// set length
content.setIntLE(startIdx, content.writerIndex() - startIdx);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2011-2024 Contributors to the Eclipse Foundation
* Copyright (c) 2011-2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
Expand Down Expand Up @@ -73,6 +73,9 @@ void decode(ByteBuf payload) {
payload.skipBytes(payload.readUnsignedShortLE());
handleLoginAck();
break;
case FEATUREEXTACK:
handleFeatureExtAck(payload);
break;
case COLMETADATA:
handleColumnMetadata(payload);
break;
Expand Down Expand Up @@ -129,6 +132,17 @@ protected void handleInfo(ByteBuf payload) {
tdsMessageCodec.chctx().fireChannelRead(info);
}

private void handleFeatureExtAck(ByteBuf payload) {
while (payload.isReadable()) {
short featureId = payload.readUnsignedByte();
if (featureId == (0xFF)) { // no more features
break;
}
// We don't keep track of supported features (e.g. JSON)
payload.skipBytes((int) payload.readUnsignedIntLE());
}
}

protected void handleLoginAck() {
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2011-2021 Contributors to the Eclipse Foundation
* Copyright (c) 2011-2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
Expand Down Expand Up @@ -90,6 +90,7 @@ public final class LoginPacket {
3FRESERVEDBIT
*/
public static final byte DEFAULT_OPTION_FLAGS3 = 0x00;
public static final byte OPTION_FLAGS3_EXTENSION = 0x10;


private byte typeFlags = 0x00;
Expand Down
Loading