Skip to content

Commit

Permalink
SQL-2259: Output UUID String representation to match ODBC driver (#278)
Browse files Browse the repository at this point in the history
* SQL-2259: Output UUID String representation to match ODBC driver

* SQL-2259: spotlessApply

* SQL-2259: Always output extjson, test changes
  • Loading branch information
bucaojit authored Aug 12, 2024
1 parent f68c471 commit 51bebc1
Show file tree
Hide file tree
Showing 12 changed files with 497 additions and 128 deletions.
1 change: 1 addition & 0 deletions .evg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ functions:
command: shell.exec
type: test
params:
shell: bash
working_dir: mongo-jdbc-driver
script: |
${PREPARE_SHELL}
Expand Down
16 changes: 16 additions & 0 deletions resources/integration_test/testdata/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,21 @@ dataset:
# undefined: {"$undefined":true},
}

- db: integration_test
collection: uuid
docsExtJson:
- _id: 0
uuid: { "$uuid": "71bf369b-2c60-4e6f-b23f-f9e88167cc96" }
type: "standard"
- _id: 1
uuid: { "$binary": { "base64": "b05gLJs2v3GWzGeB6Pk/sg==", "subType": "03" } }
type: "javalegacy"
- _id: 2
uuid: { "$binary": { "base64": "mza/cWAsb06yP/nogWfMlg==", "subType": "03" } }
type: "csharplegacy"
- _id: 3
uuid: { "$binary": { "base64": "cb82myxgTm+yP/nogWfMlg==", "subType": "03" } }
type: "pythonlegacy"

- db: integration_test
view: baz

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import static com.mongodb.jdbc.MongoDriver.MongoJDBCProperty.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import com.mongodb.jdbc.MongoConnection;
import com.mongodb.jdbc.integration.testharness.IntegrationTestUtils;
Expand All @@ -29,12 +31,15 @@
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
Expand All @@ -59,6 +64,12 @@ public class MongoIntegrationTest {
: LOCAL_HOST;
static final String DEFAULT_TEST_DB = "integration_test";
public static final String TEST_DIRECTORY = "resources/integration_test/tests";
private static final String EXPECTED_UUID =
"{\"$uuid\":\"71bf369b-2c60-4e6f-b23f-f9e88167cc96\"}";
private static final String[] UUID_REPRESENTATIONS = {
"standard", "javalegacy", "csharplegacy", "pythonlegacy", "default"
};
private static final String UUID_COLLECTION = "uuid";

private static List<TestEntry> testEntries;

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

public MongoConnection getBasicConnection(String db, Properties extraProps)
throws SQLException {
return getBasicConnection(db, extraProps, null);
}

public MongoConnection getBasicConnection(String db, Properties extraProps, String uriOptions)
throws SQLException {
String fullUrl = URL;
Properties p = new java.util.Properties(extraProps);
p.setProperty("user", System.getenv("ADF_TEST_LOCAL_USER"));
p.setProperty("password", System.getenv("ADF_TEST_LOCAL_PWD"));
p.setProperty("authSource", System.getenv("ADF_TEST_LOCAL_AUTH_DB"));
p.setProperty("database", db);
p.setProperty("ssl", "false");
return (MongoConnection) DriverManager.getConnection(URL, p);

if (uriOptions != null && !uriOptions.isEmpty()) {
fullUrl += (URL.contains("?") ? "&" : "/?") + uriOptions;
}

return (MongoConnection) DriverManager.getConnection(fullUrl, p);
}

@BeforeAll
Expand All @@ -92,7 +113,7 @@ public static void loadTestConfigs() throws IOException {
}

@TestFactory
Collection<DynamicTest> runIntegrationTests() throws SQLException {
Collection<DynamicTest> runIntegrationTests() {
List<DynamicTest> dynamicTests = new ArrayList<>();
for (TestEntry testEntry : testEntries) {
if (testEntry.skip_reason != null) {
Expand All @@ -111,7 +132,7 @@ Collection<DynamicTest> runIntegrationTests() throws SQLException {
}

/** Simple callable used to spawn a new statement and execute a query. */
public class SimpleQueryExecutor implements Callable<Void> {
public static class SimpleQueryExecutor implements Callable<Void> {
private final Connection conn;
private final String query;

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

// Connection with no logging.
MongoConnection noLogging = connect(null);
Expand Down Expand Up @@ -231,4 +252,142 @@ private void cleanUp(MongoConnection conn) {
e.printStackTrace();
}
}

/**
* Tests the handling of different UUID representations specified in the URI. The uuid fields
* have been pre-loaded into the database, stored in their respective uuid representations
* according to their type. This test verifies that each representation is correctly retrieved
* and converted to the expected string format.
*/
@Test
public void testUUIDRepresentationInURI() {
for (String representation : UUID_REPRESENTATIONS) {
System.out.println("Testing with UUID representation: " + representation);

try (MongoConnection conn =
representation.equals("default")
? getBasicConnection(DEFAULT_TEST_DB, null)
: getBasicConnection(
DEFAULT_TEST_DB,
null,
"uuidRepresentation=" + representation);
Statement stmt = conn.createStatement()) {

// If no uuidRepresentation is specified in the URI, default to `pythonlegacy`
String type = representation.equals("default") ? "pythonlegacy" : representation;
String query = "SELECT * FROM " + UUID_COLLECTION + " WHERE type = '" + type + "'";

try (ResultSet rs = stmt.executeQuery(query)) {
if (rs.next()) {
String uuid = rs.getString("uuid");
System.out.println(
"Representation: "
+ representation
+ ", Type: "
+ type
+ ", UUID: "
+ uuid);
assertEquals(
EXPECTED_UUID,
uuid,
"Mismatch for " + representation + " representation");
} else {
fail("No result found for type: " + type);
}
}
} catch (SQLException e) {
fail("Failed to execute query for " + representation + ": " + e.getMessage());
}
}
}

/**
* Tests the behavior of standard UUID representation when querying legacy UUID types. This test
* ensures that when using the standard representation, legacy UUID types are correctly
* retrieved and represented in the expected $binary format.
*/
@Test
public void testStandardRepresentationWithLegacyTypes() {
try (MongoConnection conn =
getBasicConnection(DEFAULT_TEST_DB, null, "uuidRepresentation=STANDARD");
Statement stmt = conn.createStatement()) {

for (String legacyType : UUID_REPRESENTATIONS) {
if (legacyType.equals("standard") || legacyType.equals("default")) continue;

String query =
"SELECT * FROM " + UUID_COLLECTION + " WHERE type = '" + legacyType + "'";
try (ResultSet rs = stmt.executeQuery(query)) {
if (rs.next()) {
String uuid = rs.getString("uuid");
System.out.println(
"STANDARD representation - Type: "
+ legacyType
+ ", UUID: "
+ uuid);
assertTrue(
uuid.startsWith("{\"$binary\":"),
"Expected $binary format for "
+ legacyType
+ " type with STANDARD representation");
assertTrue(
uuid.contains("\"base64\":"),
"Expected base64 field in $binary format");
assertTrue(
uuid.contains("\"subType\":"),
"Expected subType field in $binary format");
} else {
fail("No result found for type: " + legacyType);
}
}
}
} catch (SQLException e) {
fail("Failed to execute query: " + e.getMessage());
}
}

/**
* Tests the behavior of different UUID representations when querying the 'javalegacy' UUID
* type. This test verifies that each representation retrieves the 'javalegacy' UUID correctly,
* and that the value of the UUID are different.
*/
@Test
public void testDifferentRepresentationsForJavaLegacy() {
Set<String> uuidValues = new HashSet<>();
for (String representation : UUID_REPRESENTATIONS) {
if (representation.equals("default")) continue;
try (MongoConnection conn =
getBasicConnection(
DEFAULT_TEST_DB, null, "uuidRepresentation=" + representation);
Statement stmt = conn.createStatement();
ResultSet rs =
stmt.executeQuery(
"SELECT * FROM "
+ UUID_COLLECTION
+ " WHERE type = 'javalegacy'")) {
if (rs.next()) {
String uuid = rs.getString("uuid");
System.out.println(representation + " representation - UUID: " + uuid);
if (representation.equals("standard")) {
assertTrue(
uuid.startsWith("{\"$binary\":"),
"Expected $binary format for standard representation");
} else {
assertTrue(
uuid.startsWith("{\"$uuid\":"),
"Expected $uuid format for non-standard representation");
}
uuidValues.add(uuid);
} else {
fail(
"No result found for 'javalegacy' type with "
+ representation
+ " representation");
}
} catch (SQLException e) {
fail("Failed to execute query for " + representation + ": " + e.getMessage());
}
}
assertEquals(4, uuidValues.size(), "Expected 4 different UUID values (including standard)");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
public class DataLoader {
public static final String TEST_DATA_DIRECTORY = "resources/integration_test/testdata";
public static final String LOCAL_MDB_URL =
"mongodb://localhost:" + System.getenv("MDB_TEST_LOCAL_PORT");
"mongodb://localhost:"
+ System.getenv("MDB_TEST_LOCAL_PORT")
+ "/?uuidRepresentation=standard";
public static final String LOCAL_ADF_URL =
"mongodb://"
+ System.getenv("ADF_TEST_LOCAL_USER")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.bson.BsonInt32;
import org.bson.BsonValue;
import org.bson.Document;
import org.bson.UuidRepresentation;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
Expand Down Expand Up @@ -730,7 +731,10 @@ public static String compareRow(
} else if (expected_obj instanceof BsonValue) {
Object actual_obj = actualRow.getObject(i + 1);
MongoBsonValue expectedAsExtJsonValue =
new MongoBsonValue((BsonValue) expected_obj, false);
new MongoBsonValue(
(BsonValue) expected_obj,
false,
UuidRepresentation.STANDARD);
if (!expectedAsExtJsonValue.equals(actual_obj)) {
return "Expected Bson Other BsonValue value "
+ expected_obj
Expand Down
48 changes: 46 additions & 2 deletions src/main/java/com/mongodb/jdbc/MongoBsonValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@
package com.mongodb.jdbc;

import java.io.StringWriter;
import java.util.Objects;
import java.util.UUID;
import org.bson.BsonBinary;
import org.bson.BsonBinarySubType;
import org.bson.BsonDocument;
import org.bson.BsonValue;
import org.bson.UuidRepresentation;
import org.bson.codecs.BsonValueCodec;
import org.bson.codecs.EncoderContext;
import org.bson.internal.UuidHelper;
import org.bson.json.JsonMode;
import org.bson.json.JsonWriterSettings;

Expand All @@ -36,12 +42,16 @@
public class MongoBsonValue {
private JsonWriterSettings JSON_WRITER_SETTINGS;
static final EncoderContext ENCODER_CONTEXT = EncoderContext.builder().build();
private final UuidRepresentation uuidRepresentation;
private final boolean extJsonMode;

private BsonValue v;

public MongoBsonValue(BsonValue v, boolean isExtended) {
public MongoBsonValue(BsonValue v, boolean isExtended, UuidRepresentation uuidRepresentation) {
this.v = v;
this.setJsonWriterSettings(isExtended);
this.extJsonMode = isExtended;
this.uuidRepresentation = uuidRepresentation;
}

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

case ARRAY:
case BINARY:
case DATE_TIME:
case DB_POINTER:
case DECIMAL128:
Expand Down Expand Up @@ -119,6 +135,34 @@ public String toString() {
}
}

// Formats a BSON binary object into a JSON string representation of a UUID.
// If the BSON binary type is UUID_STANDARD, it directly converts it to a UUID.
// Otherwise, it uses the specified or default UUID representation to decode the binary data.
private String formatUuid(BsonBinary binary) {
UUID uuid;
byte binaryType = binary.getType();
if (binaryType == BsonBinarySubType.UUID_STANDARD.getValue()) {
uuid = binary.asUuid();
} else {
// When this.uuidRepresentation is UNSPECIFIED or null, set UuidRepresentation to PYTHON_LEGACY
UuidRepresentation representationToUse =
(Objects.nonNull(this.uuidRepresentation)
&& this.uuidRepresentation != UuidRepresentation.UNSPECIFIED)
? this.uuidRepresentation
: UuidRepresentation.PYTHON_LEGACY;
if (binaryType == BsonBinarySubType.UUID_LEGACY.getValue()
&& representationToUse == UuidRepresentation.STANDARD) {
// UUID_LEGACY subtype and trying to get the standard representation causes a BSONException,
// So we return the binary representation extended JSON instead
return toExtendedJson(binary);
}
uuid =
UuidHelper.decodeBinaryToUuid(
binary.getData(), binary.getType(), representationToUse);
}
return String.format("{\"$uuid\":\"%s\"}", uuid.toString());
}

private String toExtendedJson(BsonValue v) {
BsonValueCodec c = new BsonValueCodec();
StringWriter w = new StringWriter();
Expand Down
Loading

0 comments on commit 51bebc1

Please sign in to comment.