diff --git a/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java index 953a1a34..572f52ea 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java @@ -210,6 +210,38 @@ void testFindByPrimaryKeyWithNullValueField() { } } + @Nested + class NativeQueryTests { + + @Test + void testNative() { + var book = new Book(); + book.id = 1; + book.title = "In Search of Lost Time"; + book.author = "Marcel Proust"; + book.publishYear = 1913; + + sessionFactoryScope.inTransaction(session -> session.persist(book)); + + var nativeQuery = + """ + { + aggregate: "books", + pipeline: [ + { $match : { _id: { $eq: :id } } }, + { $project: { _id: 1, publishYear: 1, title: 1, author: 1 } } + ] + } + """; + sessionFactoryScope.inTransaction(session -> { + var query = session.createNativeQuery(nativeQuery, Book.class) + .setParameter("id", book.id); + var queriedBook = query.getSingleResult(); + assertThat(queriedBook).usingRecursiveComparison().isEqualTo(book); + }); + } + } + private static void assertCollectionContainsExactly(BsonDocument expectedDoc) { assertThat(mongoCollection.find()).containsExactly(expectedDoc); } diff --git a/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java b/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java index 444709cd..ab387faa 100644 --- a/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java +++ b/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java @@ -24,7 +24,9 @@ public final class MongoConstants { private MongoConstants() {} public static final JsonWriterSettings EXTENDED_JSON_WRITER_SETTINGS = - JsonWriterSettings.builder().outputMode(JsonMode.EXTENDED).build(); + JsonWriterSettings.builder().outputMode(JsonMode.EXTENDED) + .undefinedConverter((bsonUndefined, strictJsonWriter) -> strictJsonWriter.writeRaw("?")) + .build(); public static final String MONGO_DBMS_NAME = "MongoDB"; public static final String MONGO_JDBC_DRIVER_NAME = "MongoDB Java Driver JDBC Adapter"; diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 22df389a..85366be3 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -79,8 +79,14 @@ import java.util.List; import java.util.Optional; import java.util.Set; + +import org.bson.BsonUndefined; +import org.bson.json.Converter; +import org.bson.json.JsonMode; import org.bson.BsonValue; import org.bson.json.JsonWriter; +import org.bson.json.JsonWriterSettings; +import org.bson.json.StrictJsonWriter; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.Stack; import org.hibernate.persister.entity.EntityPersister; diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoParameterRecognizer.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoParameterRecognizer.java new file mode 100644 index 00000000..2ee6063c --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoParameterRecognizer.java @@ -0,0 +1,99 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.jdbc; + +class MongoParameterRecognizer { + + static String replace(String json) { + StringBuilder builder = new StringBuilder(json.length()); + + int i = 0; + while (i < json.length()) { + char c = json.charAt(i++); + switch (c) { + case '{': + case '}': + case '[': + case ']': + case ':': + case ',': + case ' ': + builder.append(c); + break; + case '\'': + case '"': + i = scanString(c, i, json, builder); + break; + case '?': + builder.append("{$undefined: true}"); + break; + default: + if (c == '-' || Character.isDigit(c)) { + i = scanNumber(c, i, json, builder); + } else if (c == '$' || c == '_' || Character.isLetter(c)) { + i = scanUnquotedString(c, i, json, builder); + } else { + builder.append(c); // or throw exception, as this isn't valid JSON + } + } + } + return builder.toString(); + } + + private static int scanNumber(char firstCharacter, int startIndex, String json, StringBuilder builder) { + builder.append(firstCharacter); + int i = startIndex; + char c = json.charAt(i++); + while (i < json.length() && Character.isDigit(c)) { + builder.append(c); + c = json.charAt(i++); + } + return i - 1; + } + + private static int scanUnquotedString(final char firstCharacter, final int startIndex, final String json, final StringBuilder builder) { + builder.append(firstCharacter); + int i = startIndex; + char c = json.charAt(i++); + while (i < json.length() && Character.isLetterOrDigit(c)) { + builder.append(c); + c = json.charAt(i++); + } + return i - 1; + } + + private static int scanString(final char quoteCharacter, final int startIndex, final String json, final StringBuilder builder) { + int i = startIndex; + builder.append(quoteCharacter); + while (i < json.length()) { + char c = json.charAt(i++); + if (c == '\\') { + builder.append(c); + if (i < json.length()) { + c = json.charAt(i++); + builder.append(c); + } + } else if (c == quoteCharacter) { + builder.append(c); + return i; + } else { + builder.append(c); + } + } + return i; + } +} \ No newline at end of file diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 349acd01..13035848 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -56,7 +56,7 @@ final class MongoPreparedStatement extends MongoStatement implements PreparedSta MongoDatabase mongoDatabase, ClientSession clientSession, MongoConnection mongoConnection, String mql) throws SQLSyntaxErrorException { super(mongoDatabase, clientSession, mongoConnection); - this.command = MongoStatement.parse(mql); + this.command = MongoStatement.parse(MongoParameterRecognizer.replace(mql)); this.parameterValueSetters = new ArrayList<>(); parseParameters(command, parameterValueSetters); } diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java index 46d7fc9d..88c22416 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java @@ -218,13 +218,18 @@ public double getDouble(int columnIndex) throws SQLException { @Override public ResultSetMetaData getMetaData() throws SQLException { checkClosed(); - return new MongoResultSetMetadata(); + return new MongoResultSetMetadata(fieldNames); } @Override public int findColumn(String columnLabel) throws SQLException { checkClosed(); - throw new SQLFeatureNotSupportedException("To be implemented in scope of native query tickets"); + for (int i = 0; i < fieldNames.size(); i++) { + if (fieldNames.get(i).equals(columnLabel)) { + return i + 1; + } + } + throw new SQLException("Unknown column label " + columnLabel); } @Override @@ -271,5 +276,22 @@ private void checkColumnIndex(int columnIndex) throws SQLException { } } - private static final class MongoResultSetMetadata implements ResultSetMetaDataAdapter {} + private static final class MongoResultSetMetadata implements ResultSetMetaDataAdapter { + private final List fieldNames; + + public MongoResultSetMetadata(List fieldNames) { + this.fieldNames = fieldNames; + } + + @Override + public int getColumnCount() { + return fieldNames.size(); + } + + + @Override + public String getColumnLabel(int column) { + return fieldNames.get(column - 1); + } + } } diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommandTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommandTests.java index c26f212a..81f192f1 100644 --- a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommandTests.java +++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommandTests.java @@ -41,7 +41,7 @@ void testRendering() { var expectedJson = """ - {"insert": "books", "documents": [{"title": "War and Peace", "year": {"$numberInt": "1867"}, "_id": {"$undefined": true}}]}\ + {"insert": "books", "documents": [{"title": "War and Peace", "year": {"$numberInt": "1867"}, "_id": ?}]}\ """; assertRendering(expectedJson, insertCommand); }