Skip to content

Commit 51bebc1

Browse files
authored
SQL-2259: Output UUID String representation to match ODBC driver (#278)
* SQL-2259: Output UUID String representation to match ODBC driver * SQL-2259: spotlessApply * SQL-2259: Always output extjson, test changes
1 parent f68c471 commit 51bebc1

File tree

12 files changed

+497
-128
lines changed

12 files changed

+497
-128
lines changed

.evg.yml

+1
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ functions:
355355
command: shell.exec
356356
type: test
357357
params:
358+
shell: bash
358359
working_dir: mongo-jdbc-driver
359360
script: |
360361
${PREPARE_SHELL}

resources/integration_test/testdata/integration.yml

+16
Original file line numberDiff line numberDiff line change
@@ -107,5 +107,21 @@ dataset:
107107
# undefined: {"$undefined":true},
108108
}
109109

110+
- db: integration_test
111+
collection: uuid
112+
docsExtJson:
113+
- _id: 0
114+
uuid: { "$uuid": "71bf369b-2c60-4e6f-b23f-f9e88167cc96" }
115+
type: "standard"
116+
- _id: 1
117+
uuid: { "$binary": { "base64": "b05gLJs2v3GWzGeB6Pk/sg==", "subType": "03" } }
118+
type: "javalegacy"
119+
- _id: 2
120+
uuid: { "$binary": { "base64": "mza/cWAsb06yP/nogWfMlg==", "subType": "03" } }
121+
type: "csharplegacy"
122+
- _id: 3
123+
uuid: { "$binary": { "base64": "cb82myxgTm+yP/nogWfMlg==", "subType": "03" } }
124+
type: "pythonlegacy"
125+
110126
- db: integration_test
111127
view: baz

resources/integration_test/tests/dbmd_variable_result_sets_data_methods.yml

+89-93
Large diffs are not rendered by default.

src/integration-test/java/com/mongodb/jdbc/integration/MongoIntegrationTest.java

+163-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import static com.mongodb.jdbc.MongoDriver.MongoJDBCProperty.*;
2020
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
import static org.junit.jupiter.api.Assertions.assertTrue;
22+
import static org.junit.jupiter.api.Assertions.fail;
2123

2224
import com.mongodb.jdbc.MongoConnection;
2325
import com.mongodb.jdbc.integration.testharness.IntegrationTestUtils;
@@ -29,12 +31,15 @@
2931
import java.nio.file.Paths;
3032
import java.sql.Connection;
3133
import java.sql.DriverManager;
34+
import java.sql.ResultSet;
3235
import java.sql.SQLException;
3336
import java.sql.Statement;
3437
import java.util.ArrayList;
3538
import java.util.Collection;
39+
import java.util.HashSet;
3640
import java.util.List;
3741
import java.util.Properties;
42+
import java.util.Set;
3843
import java.util.concurrent.Callable;
3944
import java.util.concurrent.ExecutorService;
4045
import java.util.concurrent.Executors;
@@ -59,6 +64,12 @@ public class MongoIntegrationTest {
5964
: LOCAL_HOST;
6065
static final String DEFAULT_TEST_DB = "integration_test";
6166
public static final String TEST_DIRECTORY = "resources/integration_test/tests";
67+
private static final String EXPECTED_UUID =
68+
"{\"$uuid\":\"71bf369b-2c60-4e6f-b23f-f9e88167cc96\"}";
69+
private static final String[] UUID_REPRESENTATIONS = {
70+
"standard", "javalegacy", "csharplegacy", "pythonlegacy", "default"
71+
};
72+
private static final String UUID_COLLECTION = "uuid";
6273

6374
private static List<TestEntry> testEntries;
6475

@@ -76,14 +87,24 @@ public MongoConnection getBasicConnection(Properties extraProps) throws SQLExcep
7687

7788
public MongoConnection getBasicConnection(String db, Properties extraProps)
7889
throws SQLException {
90+
return getBasicConnection(db, extraProps, null);
91+
}
7992

93+
public MongoConnection getBasicConnection(String db, Properties extraProps, String uriOptions)
94+
throws SQLException {
95+
String fullUrl = URL;
8096
Properties p = new java.util.Properties(extraProps);
8197
p.setProperty("user", System.getenv("ADF_TEST_LOCAL_USER"));
8298
p.setProperty("password", System.getenv("ADF_TEST_LOCAL_PWD"));
8399
p.setProperty("authSource", System.getenv("ADF_TEST_LOCAL_AUTH_DB"));
84100
p.setProperty("database", db);
85101
p.setProperty("ssl", "false");
86-
return (MongoConnection) DriverManager.getConnection(URL, p);
102+
103+
if (uriOptions != null && !uriOptions.isEmpty()) {
104+
fullUrl += (URL.contains("?") ? "&" : "/?") + uriOptions;
105+
}
106+
107+
return (MongoConnection) DriverManager.getConnection(fullUrl, p);
87108
}
88109

89110
@BeforeAll
@@ -92,7 +113,7 @@ public static void loadTestConfigs() throws IOException {
92113
}
93114

94115
@TestFactory
95-
Collection<DynamicTest> runIntegrationTests() throws SQLException {
116+
Collection<DynamicTest> runIntegrationTests() {
96117
List<DynamicTest> dynamicTests = new ArrayList<>();
97118
for (TestEntry testEntry : testEntries) {
98119
if (testEntry.skip_reason != null) {
@@ -111,7 +132,7 @@ Collection<DynamicTest> runIntegrationTests() throws SQLException {
111132
}
112133

113134
/** Simple callable used to spawn a new statement and execute a query. */
114-
public class SimpleQueryExecutor implements Callable<Void> {
135+
public static class SimpleQueryExecutor implements Callable<Void> {
115136
private final Connection conn;
116137
private final String query;
117138

@@ -137,7 +158,7 @@ public Void call() throws Exception {
137158
@Test
138159
public void testLoggingWithParallelConnectionAndStatementExec() throws Exception {
139160
ExecutorService executor = Executors.newFixedThreadPool(4);
140-
List<Callable<Void>> tasks = new ArrayList<Callable<Void>>();
161+
List<Callable<Void>> tasks = new ArrayList<>();
141162

142163
// Connection with no logging.
143164
MongoConnection noLogging = connect(null);
@@ -231,4 +252,142 @@ private void cleanUp(MongoConnection conn) {
231252
e.printStackTrace();
232253
}
233254
}
255+
256+
/**
257+
* Tests the handling of different UUID representations specified in the URI. The uuid fields
258+
* have been pre-loaded into the database, stored in their respective uuid representations
259+
* according to their type. This test verifies that each representation is correctly retrieved
260+
* and converted to the expected string format.
261+
*/
262+
@Test
263+
public void testUUIDRepresentationInURI() {
264+
for (String representation : UUID_REPRESENTATIONS) {
265+
System.out.println("Testing with UUID representation: " + representation);
266+
267+
try (MongoConnection conn =
268+
representation.equals("default")
269+
? getBasicConnection(DEFAULT_TEST_DB, null)
270+
: getBasicConnection(
271+
DEFAULT_TEST_DB,
272+
null,
273+
"uuidRepresentation=" + representation);
274+
Statement stmt = conn.createStatement()) {
275+
276+
// If no uuidRepresentation is specified in the URI, default to `pythonlegacy`
277+
String type = representation.equals("default") ? "pythonlegacy" : representation;
278+
String query = "SELECT * FROM " + UUID_COLLECTION + " WHERE type = '" + type + "'";
279+
280+
try (ResultSet rs = stmt.executeQuery(query)) {
281+
if (rs.next()) {
282+
String uuid = rs.getString("uuid");
283+
System.out.println(
284+
"Representation: "
285+
+ representation
286+
+ ", Type: "
287+
+ type
288+
+ ", UUID: "
289+
+ uuid);
290+
assertEquals(
291+
EXPECTED_UUID,
292+
uuid,
293+
"Mismatch for " + representation + " representation");
294+
} else {
295+
fail("No result found for type: " + type);
296+
}
297+
}
298+
} catch (SQLException e) {
299+
fail("Failed to execute query for " + representation + ": " + e.getMessage());
300+
}
301+
}
302+
}
303+
304+
/**
305+
* Tests the behavior of standard UUID representation when querying legacy UUID types. This test
306+
* ensures that when using the standard representation, legacy UUID types are correctly
307+
* retrieved and represented in the expected $binary format.
308+
*/
309+
@Test
310+
public void testStandardRepresentationWithLegacyTypes() {
311+
try (MongoConnection conn =
312+
getBasicConnection(DEFAULT_TEST_DB, null, "uuidRepresentation=STANDARD");
313+
Statement stmt = conn.createStatement()) {
314+
315+
for (String legacyType : UUID_REPRESENTATIONS) {
316+
if (legacyType.equals("standard") || legacyType.equals("default")) continue;
317+
318+
String query =
319+
"SELECT * FROM " + UUID_COLLECTION + " WHERE type = '" + legacyType + "'";
320+
try (ResultSet rs = stmt.executeQuery(query)) {
321+
if (rs.next()) {
322+
String uuid = rs.getString("uuid");
323+
System.out.println(
324+
"STANDARD representation - Type: "
325+
+ legacyType
326+
+ ", UUID: "
327+
+ uuid);
328+
assertTrue(
329+
uuid.startsWith("{\"$binary\":"),
330+
"Expected $binary format for "
331+
+ legacyType
332+
+ " type with STANDARD representation");
333+
assertTrue(
334+
uuid.contains("\"base64\":"),
335+
"Expected base64 field in $binary format");
336+
assertTrue(
337+
uuid.contains("\"subType\":"),
338+
"Expected subType field in $binary format");
339+
} else {
340+
fail("No result found for type: " + legacyType);
341+
}
342+
}
343+
}
344+
} catch (SQLException e) {
345+
fail("Failed to execute query: " + e.getMessage());
346+
}
347+
}
348+
349+
/**
350+
* Tests the behavior of different UUID representations when querying the 'javalegacy' UUID
351+
* type. This test verifies that each representation retrieves the 'javalegacy' UUID correctly,
352+
* and that the value of the UUID are different.
353+
*/
354+
@Test
355+
public void testDifferentRepresentationsForJavaLegacy() {
356+
Set<String> uuidValues = new HashSet<>();
357+
for (String representation : UUID_REPRESENTATIONS) {
358+
if (representation.equals("default")) continue;
359+
try (MongoConnection conn =
360+
getBasicConnection(
361+
DEFAULT_TEST_DB, null, "uuidRepresentation=" + representation);
362+
Statement stmt = conn.createStatement();
363+
ResultSet rs =
364+
stmt.executeQuery(
365+
"SELECT * FROM "
366+
+ UUID_COLLECTION
367+
+ " WHERE type = 'javalegacy'")) {
368+
if (rs.next()) {
369+
String uuid = rs.getString("uuid");
370+
System.out.println(representation + " representation - UUID: " + uuid);
371+
if (representation.equals("standard")) {
372+
assertTrue(
373+
uuid.startsWith("{\"$binary\":"),
374+
"Expected $binary format for standard representation");
375+
} else {
376+
assertTrue(
377+
uuid.startsWith("{\"$uuid\":"),
378+
"Expected $uuid format for non-standard representation");
379+
}
380+
uuidValues.add(uuid);
381+
} else {
382+
fail(
383+
"No result found for 'javalegacy' type with "
384+
+ representation
385+
+ " representation");
386+
}
387+
} catch (SQLException e) {
388+
fail("Failed to execute query for " + representation + ": " + e.getMessage());
389+
}
390+
}
391+
assertEquals(4, uuidValues.size(), "Expected 4 different UUID values (including standard)");
392+
}
234393
}

src/integration-test/java/com/mongodb/jdbc/integration/testharness/DataLoader.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@
5353
public class DataLoader {
5454
public static final String TEST_DATA_DIRECTORY = "resources/integration_test/testdata";
5555
public static final String LOCAL_MDB_URL =
56-
"mongodb://localhost:" + System.getenv("MDB_TEST_LOCAL_PORT");
56+
"mongodb://localhost:"
57+
+ System.getenv("MDB_TEST_LOCAL_PORT")
58+
+ "/?uuidRepresentation=standard";
5759
public static final String LOCAL_ADF_URL =
5860
"mongodb://"
5961
+ System.getenv("ADF_TEST_LOCAL_USER")

src/integration-test/java/com/mongodb/jdbc/integration/testharness/IntegrationTestUtils.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import org.bson.BsonInt32;
5151
import org.bson.BsonValue;
5252
import org.bson.Document;
53+
import org.bson.UuidRepresentation;
5354
import org.yaml.snakeyaml.LoaderOptions;
5455
import org.yaml.snakeyaml.Yaml;
5556
import org.yaml.snakeyaml.constructor.Constructor;
@@ -730,7 +731,10 @@ public static String compareRow(
730731
} else if (expected_obj instanceof BsonValue) {
731732
Object actual_obj = actualRow.getObject(i + 1);
732733
MongoBsonValue expectedAsExtJsonValue =
733-
new MongoBsonValue((BsonValue) expected_obj, false);
734+
new MongoBsonValue(
735+
(BsonValue) expected_obj,
736+
false,
737+
UuidRepresentation.STANDARD);
734738
if (!expectedAsExtJsonValue.equals(actual_obj)) {
735739
return "Expected Bson Other BsonValue value "
736740
+ expected_obj

src/main/java/com/mongodb/jdbc/MongoBsonValue.java

+46-2
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,16 @@
1717
package com.mongodb.jdbc;
1818

1919
import java.io.StringWriter;
20+
import java.util.Objects;
21+
import java.util.UUID;
22+
import org.bson.BsonBinary;
23+
import org.bson.BsonBinarySubType;
2024
import org.bson.BsonDocument;
2125
import org.bson.BsonValue;
26+
import org.bson.UuidRepresentation;
2227
import org.bson.codecs.BsonValueCodec;
2328
import org.bson.codecs.EncoderContext;
29+
import org.bson.internal.UuidHelper;
2430
import org.bson.json.JsonMode;
2531
import org.bson.json.JsonWriterSettings;
2632

@@ -36,12 +42,16 @@
3642
public class MongoBsonValue {
3743
private JsonWriterSettings JSON_WRITER_SETTINGS;
3844
static final EncoderContext ENCODER_CONTEXT = EncoderContext.builder().build();
45+
private final UuidRepresentation uuidRepresentation;
46+
private final boolean extJsonMode;
3947

4048
private BsonValue v;
4149

42-
public MongoBsonValue(BsonValue v, boolean isExtended) {
50+
public MongoBsonValue(BsonValue v, boolean isExtended, UuidRepresentation uuidRepresentation) {
4351
this.v = v;
4452
this.setJsonWriterSettings(isExtended);
53+
this.extJsonMode = isExtended;
54+
this.uuidRepresentation = uuidRepresentation;
4555
}
4656

4757
public void setJsonWriterSettings(boolean isExtended) {
@@ -75,9 +85,15 @@ public String toString() {
7585
// those quotes in the output of this method, so we simply
7686
// return the underlying String value.
7787
return this.v.asString().getValue();
88+
case BINARY:
89+
BsonBinary binary = this.v.asBinary();
90+
if (binary.getType() == BsonBinarySubType.UUID_STANDARD.getValue()
91+
|| binary.getType() == BsonBinarySubType.UUID_LEGACY.getValue()) {
92+
return formatUuid(binary);
93+
}
94+
// Fall through to toExtendedJson(this.v) for other binary types
7895

7996
case ARRAY:
80-
case BINARY:
8197
case DATE_TIME:
8298
case DB_POINTER:
8399
case DECIMAL128:
@@ -119,6 +135,34 @@ public String toString() {
119135
}
120136
}
121137

138+
// Formats a BSON binary object into a JSON string representation of a UUID.
139+
// If the BSON binary type is UUID_STANDARD, it directly converts it to a UUID.
140+
// Otherwise, it uses the specified or default UUID representation to decode the binary data.
141+
private String formatUuid(BsonBinary binary) {
142+
UUID uuid;
143+
byte binaryType = binary.getType();
144+
if (binaryType == BsonBinarySubType.UUID_STANDARD.getValue()) {
145+
uuid = binary.asUuid();
146+
} else {
147+
// When this.uuidRepresentation is UNSPECIFIED or null, set UuidRepresentation to PYTHON_LEGACY
148+
UuidRepresentation representationToUse =
149+
(Objects.nonNull(this.uuidRepresentation)
150+
&& this.uuidRepresentation != UuidRepresentation.UNSPECIFIED)
151+
? this.uuidRepresentation
152+
: UuidRepresentation.PYTHON_LEGACY;
153+
if (binaryType == BsonBinarySubType.UUID_LEGACY.getValue()
154+
&& representationToUse == UuidRepresentation.STANDARD) {
155+
// UUID_LEGACY subtype and trying to get the standard representation causes a BSONException,
156+
// So we return the binary representation extended JSON instead
157+
return toExtendedJson(binary);
158+
}
159+
uuid =
160+
UuidHelper.decodeBinaryToUuid(
161+
binary.getData(), binary.getType(), representationToUse);
162+
}
163+
return String.format("{\"$uuid\":\"%s\"}", uuid.toString());
164+
}
165+
122166
private String toExtendedJson(BsonValue v) {
123167
BsonValueCodec c = new BsonValueCodec();
124168
StringWriter w = new StringWriter();

0 commit comments

Comments
 (0)