Skip to content

Commit 581501a

Browse files
authored
JSON support for MSSQL (#1681)
* JSON support for MSSQL Closes #1680 Signed-off-by: Thomas Segismont <tsegismont@gmail.com> * Avoid redundant JSON to string conversion When creating the param definition, the JSON payload was converted to string. And then converted again when encoding the param. In fact, MSSQL allows to send JSON as NVARCHAR(MAX) regardless of the size. And then we can have a single conversion when encoding the param. Signed-off-by: Thomas Segismont <tsegismont@gmail.com> --------- Signed-off-by: Thomas Segismont <tsegismont@gmail.com>
1 parent 775a207 commit 581501a

9 files changed

Lines changed: 359 additions & 15 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: 50 additions & 6 deletions
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;
@@ -870,11 +873,6 @@ public void encodeParam(ByteBuf byteBuf, String name, boolean out, Object value)
870873
byteBuf.writeCharSequence(val, StandardCharsets.UTF_16LE);
871874
}
872875
}
873-
874-
private void writeCollation(ByteBuf byteBuf) {
875-
byteBuf.writeInt(0x0904d000);
876-
byteBuf.writeByte(0x34);
877-
}
878876
},
879877
NCHAR(0xEF) {
880878
@Override
@@ -969,6 +967,45 @@ public Object decodeValue(ByteBuf byteBuf, TypeInfo typeInfo) {
969967
return result;
970968
}
971969
},
970+
JSON(0xF4) {
971+
@Override
972+
public TypeInfo decodeTypeInfo(ByteBuf byteBuf) {
973+
return new TypeInfo().maxLength(0xFFFF);
974+
}
975+
976+
@Override
977+
public JDBCType jdbcType(TypeInfo typeInfo) {
978+
return JDBCType.OTHER;
979+
}
980+
981+
@Override
982+
public Object decodeValue(ByteBuf byteBuf, TypeInfo typeInfo) {
983+
long payloadLength = byteBuf.readLongLE();
984+
if (isPLPNull(payloadLength)) {
985+
return null;
986+
}
987+
String jsonString = readPLP(byteBuf).toString(StandardCharsets.UTF_16LE);
988+
return Json.decodeValue(jsonString);
989+
}
990+
991+
@Override
992+
public String paramDefinition(Object value) {
993+
return "nvarchar(max)";
994+
}
995+
996+
@Override
997+
public void encodeParam(ByteBuf byteBuf, String name, boolean out, Object value) {
998+
writeParamDescription(byteBuf, name, out, NVARCHAR.id);
999+
String val = value.toString();
1000+
byteBuf.writeShortLE(0xFFFF);
1001+
writeCollation(byteBuf);
1002+
byteBuf.writeLongLE(val.length() * 2L);
1003+
byteBuf.writeIntLE(val.length() * 2);
1004+
byteBuf.writeCharSequence(val, StandardCharsets.UTF_16LE);
1005+
byteBuf.writeIntLE(0);
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) {
@@ -1173,4 +1212,9 @@ private static int daysFromStartDate(LocalDate localDate) {
11731212
private static long hundredsOfNanos(LocalTime localTime) {
11741213
return localTime.toNanoOfDay() / 100;
11751214
}
1215+
1216+
private static void writeCollation(ByteBuf byteBuf) {
1217+
byteBuf.writeInt(0x0904d000);
1218+
byteBuf.writeByte(0x34);
1219+
}
11761220
}

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

Lines changed: 17 additions & 6 deletions
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
@@ -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: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2011-2024 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
@@ -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: 2 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
@@ -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)