Skip to content

Commit a97a5d6

Browse files
committed
JSON support for MSSQLRule
Closes #1680 Signed-off-by: Thomas Segismont <tsegismont@gmail.com>
1 parent 775a207 commit a97a5d6

9 files changed

Lines changed: 346 additions & 7 deletions

File tree

vertx-mssql-client/src/main/asciidoc/index.adoc

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,10 @@ Currently the client supports the following SQL Server types:
250250
| `GUID`
251251
| `java.util.UUID`
252252
|
253+
254+
| `json`
255+
| `io.vertx.core.json.JsonObject` / `io.vertx.core.json.JsonArray`
256+
| Native JSON type (SQL Server 2025+). See <<handling-json>>
253257
|===
254258

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

297+
[[handling-json]]
298+
=== Handling JSON
299+
300+
SQL Server 2025 introduces a native `json` data type for storing JSON documents.
301+
302+
IMPORTANT: The native `json` type requires SQL Server 2025 or later and only supports JSON objects and arrays.
303+
Scalars, booleans, and the JSON null literal are not supported.
304+
305+
The `json` data type is represented by the following Java types:
306+
307+
- `io.vertx.core.json.JsonObject`
308+
- `io.vertx.core.json.JsonArray`
309+
310+
[source,$lang]
311+
----
312+
{@link examples.MSSQLClientExamples#jsonExample(io.vertx.sqlclient.SqlClient)}
313+
----
314+
315+
[source,$lang]
316+
----
317+
{@link examples.MSSQLClientExamples#jsonArrayExample(io.vertx.sqlclient.SqlClient)}
318+
----
319+
320+
NOTE: Attempting to insert scalar values, booleans, or {@link io.vertx.sqlclient.Tuple#JSON_NULL} will result in an error.
321+
293322
== Collector queries
294323

295324
You can use Java collectors with the query API:

vertx-mssql-client/src/main/java/examples/MSSQLClientExamples.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
package examples;
1313

1414
import io.vertx.core.Vertx;
15+
import io.vertx.core.json.JsonArray;
16+
import io.vertx.core.json.JsonObject;
1517
import io.vertx.core.net.ClientSSLOptions;
1618
import io.vertx.core.net.PemTrustOptions;
1719
import io.vertx.docgen.Source;
@@ -353,4 +355,42 @@ public void infoHandler(MSSQLConnection connection) {
353355
System.out.println("Received info " + info.getSeverity() + "" + info.getMessage());
354356
});
355357
}
358+
359+
public void jsonExample(SqlClient client) {
360+
JsonObject json = new JsonObject()
361+
.put("name", "Alice")
362+
.put("age", 30);
363+
364+
client
365+
.preparedQuery("INSERT INTO users (id, data) VALUES (@p1, @p2)")
366+
.execute(Tuple.of(1, json))
367+
.compose(v -> client
368+
.preparedQuery("SELECT data FROM users WHERE id = @p1")
369+
.execute(Tuple.of(1)))
370+
.onComplete(ar -> {
371+
if (ar.succeeded()) {
372+
Row row = ar.result().iterator().next();
373+
JsonObject data = row.getJsonObject(0);
374+
System.out.println("Received: " + data.getString("name"));
375+
}
376+
});
377+
}
378+
379+
public void jsonArrayExample(SqlClient client) {
380+
JsonArray tags = new JsonArray().add("admin").add("user");
381+
382+
client
383+
.preparedQuery("INSERT INTO users (id, data) VALUES (@p1, @p2)")
384+
.execute(Tuple.of(2, tags))
385+
.compose(v -> client
386+
.preparedQuery("SELECT data FROM users WHERE id = @p1")
387+
.execute(Tuple.of(2)))
388+
.onComplete(ar -> {
389+
if (ar.succeeded()) {
390+
Row row = ar.result().iterator().next();
391+
JsonArray data = row.getJsonArray(0);
392+
System.out.println("Received: " + data);
393+
}
394+
});
395+
}
356396
}

vertx-mssql-client/src/main/java/io/vertx/mssqlclient/impl/MSSQLRow.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2011-2021 Contributors to the Eclipse Foundation
2+
* Copyright (c) 2011-2026 Contributors to the Eclipse Foundation
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License 2.0 which is available at
@@ -12,6 +12,8 @@
1212
package io.vertx.mssqlclient.impl;
1313

1414
import io.vertx.core.buffer.Buffer;
15+
import io.vertx.core.json.JsonArray;
16+
import io.vertx.core.json.JsonObject;
1517
import io.vertx.sqlclient.impl.RowBase;
1618
import io.vertx.sqlclient.internal.RowDescriptorBase;
1719

@@ -59,6 +61,10 @@ public <T> T get(Class<T> type, int position) {
5961
return type.cast(getOffsetDateTime(position));
6062
} else if (type == Buffer.class) {
6163
return type.cast(getBuffer(position));
64+
} else if (type == JsonObject.class) {
65+
return type.cast(getJsonObject(position));
66+
} else if (type == JsonArray.class) {
67+
return type.cast(getJsonArray(position));
6268
} else if (type == UUID.class) {
6369
return type.cast(getValue(position));
6470
} else if (type == Object.class) {

vertx-mssql-client/src/main/java/io/vertx/mssqlclient/impl/codec/DataType.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2011-2022 Contributors to the Eclipse Foundation
2+
* Copyright (c) 2011-2026 Contributors to the Eclipse Foundation
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License 2.0 which is available at
@@ -18,6 +18,9 @@
1818
import io.netty.util.collection.IntObjectMap;
1919
import io.vertx.core.buffer.Buffer;
2020
import io.vertx.core.internal.buffer.BufferInternal;
21+
import io.vertx.core.json.Json;
22+
import io.vertx.core.json.JsonArray;
23+
import io.vertx.core.json.JsonObject;
2124

2225
import java.math.BigDecimal;
2326
import java.math.BigInteger;
@@ -969,6 +972,40 @@ public Object decodeValue(ByteBuf byteBuf, TypeInfo typeInfo) {
969972
return result;
970973
}
971974
},
975+
JSON(0xF4) {
976+
@Override
977+
public TypeInfo decodeTypeInfo(ByteBuf byteBuf) {
978+
return new TypeInfo().maxLength(0xFFFF);
979+
}
980+
981+
@Override
982+
public JDBCType jdbcType(TypeInfo typeInfo) {
983+
return JDBCType.OTHER;
984+
}
985+
986+
@Override
987+
public Object decodeValue(ByteBuf byteBuf, TypeInfo typeInfo) {
988+
long payloadLength = byteBuf.readLongLE();
989+
if (isPLPNull(payloadLength)) {
990+
return null;
991+
}
992+
String jsonString = readPLP(byteBuf).toString(StandardCharsets.UTF_16LE);
993+
return Json.decodeValue(jsonString);
994+
}
995+
996+
@Override
997+
public String paramDefinition(Object value) {
998+
// SQL Server expects text for JSON params
999+
return NVARCHAR.paramDefinition(value);
1000+
}
1001+
1002+
@Override
1003+
public void encodeParam(ByteBuf byteBuf, String name, boolean out, Object value) {
1004+
// SQL Server expects text for JSON params
1005+
NVARCHAR.encodeParam(byteBuf, name, out, value);
1006+
}
1007+
},
1008+
9721009
SSVARIANT(0x62) {
9731010
@Override
9741011
public TypeInfo decodeTypeInfo(ByteBuf byteBuf) {
@@ -1108,6 +1145,8 @@ public void encodeParam(ByteBuf byteBuf, String name, boolean out, Object value)
11081145
typesByValueClass.put(OffsetDateTime.class, DATETIMEOFFSETN);
11091146
typesByValueClass.put(UUID.class, GUID);
11101147
typesByValueClass.put(Buffer.class, BIGVARBINARY);
1148+
typesByValueClass.put(JsonObject.class, JSON);
1149+
typesByValueClass.put(JsonArray.class, JSON);
11111150
}
11121151

11131152
public static DataType forId(int id) {

vertx-mssql-client/src/main/java/io/vertx/mssqlclient/impl/codec/InitMSSQLCommandMessage.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ void encode() {
5656
LoginPacket.OPTION_FLAGS2_ODBC_ON
5757
); // OptionFlags2
5858
content.writeByte(LoginPacket.DEFAULT_TYPE_FLAGS); // TypeFlags
59-
content.writeByte(LoginPacket.DEFAULT_OPTION_FLAGS3); // OptionFlags3
59+
content.writeByte(LoginPacket.DEFAULT_OPTION_FLAGS3 | LoginPacket.OPTION_FLAGS3_EXTENSION); // OptionFlags3
6060
content.writeZero(8); // ClientTimeZone + ClientLCID
6161

6262
/*
@@ -98,9 +98,10 @@ void encode() {
9898
content.writeZero(2); // offset
9999
content.writeShortLE(serverName.length());
100100

101-
// Unused or Extension
102-
int unusedOffsetLengthIdx = content.writerIndex();
103-
content.writeZero(4);
101+
// Extension
102+
int extensionOffsetLengthIdx = content.writerIndex();
103+
content.writeZero(2); // offset
104+
content.writeShortLE(4);
104105

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

161-
content.setShortLE(unusedOffsetLengthIdx, content.writerIndex() - startIdx);
162+
content.setShortLE(extensionOffsetLengthIdx, content.writerIndex() - startIdx);
163+
int featureExtOffsetIdx = content.writerIndex();
164+
content.writeIntLE(0);
162165

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

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

180+
content.setIntLE(featureExtOffsetIdx, content.writerIndex() - startIdx);
181+
// TDS_FEATURE_EXT_JSONSUPPORT
182+
content.writeByte(0x0D); // feature ID
183+
content.writeIntLE(1); // data length
184+
content.writeByte(0x01); // JSON support version 1
185+
// FEATUREEXT terminator
186+
content.writeByte(0xFF);
187+
177188
// set length
178189
content.setIntLE(startIdx, content.writerIndex() - startIdx);
179190

vertx-mssql-client/src/main/java/io/vertx/mssqlclient/impl/codec/MSSQLCommandMessage.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ void decode(ByteBuf payload) {
7373
payload.skipBytes(payload.readUnsignedShortLE());
7474
handleLoginAck();
7575
break;
76+
case FEATUREEXTACK:
77+
handleFeatureExtAck(payload);
78+
break;
7679
case COLMETADATA:
7780
handleColumnMetadata(payload);
7881
break;
@@ -129,6 +132,17 @@ protected void handleInfo(ByteBuf payload) {
129132
tdsMessageCodec.chctx().fireChannelRead(info);
130133
}
131134

135+
private void handleFeatureExtAck(ByteBuf payload) {
136+
while (payload.isReadable()) {
137+
short featureId = payload.readUnsignedByte();
138+
if (featureId == (0xFF)) { // no more features
139+
break;
140+
}
141+
// We don't keep track of supported features (e.g. JSON)
142+
payload.skipBytes((int) payload.readUnsignedIntLE());
143+
}
144+
}
145+
132146
protected void handleLoginAck() {
133147
}
134148

vertx-mssql-client/src/main/java/io/vertx/mssqlclient/impl/protocol/client/login/LoginPacket.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ public final class LoginPacket {
9090
3FRESERVEDBIT
9191
*/
9292
public static final byte DEFAULT_OPTION_FLAGS3 = 0x00;
93+
public static final byte OPTION_FLAGS3_EXTENSION = 0x10;
9394

9495

9596
private byte typeFlags = 0x00;

0 commit comments

Comments
 (0)