Skip to content

Commit 7c6b11f

Browse files
committed
WIP: Use "?" for parameter markers in both native query and query translation
1 parent 011e6e4 commit 7c6b11f

File tree

5 files changed

+163
-5
lines changed

5 files changed

+163
-5
lines changed

src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,37 @@ void testGetByPrimaryKeyWithNullValueField() {
237237
}
238238
}
239239

240+
@Nested
241+
class NativeQueryTests {
242+
243+
@Test
244+
void testNative() {
245+
var book = new Book();
246+
book.id = 1;
247+
book.title = "In Search of Lost Time";
248+
book.publishYear = 1913;
249+
250+
sessionFactoryScope.inTransaction(session -> session.persist(book));
251+
252+
var nativeQuery =
253+
"""
254+
{
255+
aggregate: "books",
256+
pipeline: [
257+
{ $match : { _id: { $eq: :id } } },
258+
{ $project: { _id: 1, publishYear: 1, title: 1, author: 1 } }
259+
]
260+
}
261+
""";
262+
sessionFactoryScope.inTransaction(session -> {
263+
var query = session.createNativeQuery(nativeQuery, Book.class)
264+
.setParameter("id", book.id);
265+
var queriedBook = query.getSingleResult();
266+
assertThat(queriedBook).usingRecursiveComparison().isEqualTo(book);
267+
});
268+
}
269+
}
270+
240271
private static void assertCollectionContainsExactly(BsonDocument expectedDoc) {
241272
assertThat(mongoCollection.find()).containsExactly(expectedDoc);
242273
}

src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,13 @@
5656
import java.util.HashSet;
5757
import java.util.List;
5858
import java.util.Set;
59+
60+
import org.bson.BsonUndefined;
61+
import org.bson.json.Converter;
5962
import org.bson.json.JsonMode;
6063
import org.bson.json.JsonWriter;
6164
import org.bson.json.JsonWriterSettings;
65+
import org.bson.json.StrictJsonWriter;
6266
import org.hibernate.engine.spi.SessionFactoryImplementor;
6367
import org.hibernate.internal.util.collections.Stack;
6468
import org.hibernate.persister.entity.EntityPersister;
@@ -151,7 +155,9 @@
151155

152156
abstract class AbstractMqlTranslator<T extends JdbcOperation> implements SqlAstTranslator<T> {
153157
private static final JsonWriterSettings JSON_WRITER_SETTINGS =
154-
JsonWriterSettings.builder().outputMode(JsonMode.EXTENDED).build();
158+
JsonWriterSettings.builder().outputMode(JsonMode.EXTENDED)
159+
.undefinedConverter((bsonUndefined, strictJsonWriter) -> strictJsonWriter.writeRaw("?"))
160+
.build();
155161

156162
private final SessionFactoryImplementor sessionFactory;
157163

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2025-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mongodb.hibernate.jdbc;
18+
19+
class MongoParameterRecognizer {
20+
21+
static String replace(String json) {
22+
StringBuilder builder = new StringBuilder(json.length());
23+
24+
int i = 0;
25+
while (i < json.length()) {
26+
char c = json.charAt(i++);
27+
switch (c) {
28+
case '{':
29+
case '}':
30+
case '[':
31+
case ']':
32+
case ':':
33+
case ',':
34+
case ' ':
35+
builder.append(c);
36+
break;
37+
case '\'':
38+
case '"':
39+
i = scanString(c, i, json, builder);
40+
break;
41+
case '?':
42+
builder.append("{$undefined: true}");
43+
break;
44+
default:
45+
if (c == '-' || Character.isDigit(c)) {
46+
i = scanNumber(c, i, json, builder);
47+
} else if (c == '$' || c == '_' || Character.isLetter(c)) {
48+
i = scanUnquotedString(c, i, json, builder);
49+
} else {
50+
builder.append(c); // or throw exception, as this isn't valid JSON
51+
}
52+
}
53+
}
54+
return builder.toString();
55+
}
56+
57+
private static int scanNumber(char firstCharacter, int startIndex, String json, StringBuilder builder) {
58+
builder.append(firstCharacter);
59+
int i = startIndex;
60+
char c = json.charAt(i++);
61+
while (i < json.length() && Character.isDigit(c)) {
62+
builder.append(c);
63+
c = json.charAt(i++);
64+
}
65+
return i - 1;
66+
}
67+
68+
private static int scanUnquotedString(final char firstCharacter, final int startIndex, final String json, final StringBuilder builder) {
69+
builder.append(firstCharacter);
70+
int i = startIndex;
71+
char c = json.charAt(i++);
72+
while (i < json.length() && Character.isLetterOrDigit(c)) {
73+
builder.append(c);
74+
c = json.charAt(i++);
75+
}
76+
return i - 1;
77+
}
78+
79+
private static int scanString(final char quoteCharacter, final int startIndex, final String json, final StringBuilder builder) {
80+
int i = startIndex;
81+
builder.append(quoteCharacter);
82+
while (i < json.length()) {
83+
char c = json.charAt(i++);
84+
if (c == '\\') {
85+
builder.append(c);
86+
if (i < json.length()) {
87+
c = json.charAt(i++);
88+
builder.append(c);
89+
}
90+
} else if (c == quoteCharacter) {
91+
builder.append(c);
92+
return i;
93+
} else {
94+
builder.append(c);
95+
}
96+
}
97+
return i;
98+
}
99+
}

src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ final class MongoPreparedStatement extends MongoStatement implements PreparedSta
6161
MongoDatabase mongoDatabase, ClientSession clientSession, MongoConnection mongoConnection, String mql)
6262
throws SQLSyntaxErrorException {
6363
super(mongoDatabase, clientSession, mongoConnection);
64-
this.command = MongoStatement.parse(mql);
64+
this.command = MongoStatement.parse(MongoParameterRecognizer.replace(mql));
6565
this.parameterValueSetters = new ArrayList<>();
6666
parseParameters(command, parameterValueSetters);
6767
}

src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,13 +210,18 @@ public double getDouble(int columnIndex) throws SQLException {
210210
@Override
211211
public ResultSetMetaData getMetaData() throws SQLException {
212212
checkClosed();
213-
return new MongoResultSetMetadata();
213+
return new MongoResultSetMetadata(fieldNames);
214214
}
215215

216216
@Override
217217
public int findColumn(String columnLabel) throws SQLException {
218218
checkClosed();
219-
throw new SQLFeatureNotSupportedException("To be implemented in scope of native query tickets");
219+
for (int i = 0; i < fieldNames.size(); i++) {
220+
if (fieldNames.get(i).equals(columnLabel)) {
221+
return i + 1;
222+
}
223+
}
224+
throw new SQLException("Unknown column label " + columnLabel);
220225
}
221226

222227
@Override
@@ -263,5 +268,22 @@ private void checkColumnIndex(int columnIndex) throws SQLException {
263268
}
264269
}
265270

266-
private static final class MongoResultSetMetadata implements ResultSetMetaDataAdapter {}
271+
private static final class MongoResultSetMetadata implements ResultSetMetaDataAdapter {
272+
private final List<String> fieldNames;
273+
274+
public MongoResultSetMetadata(List<String> fieldNames) {
275+
this.fieldNames = fieldNames;
276+
}
277+
278+
@Override
279+
public int getColumnCount() {
280+
return fieldNames.size();
281+
}
282+
283+
284+
@Override
285+
public String getColumnLabel(int column) {
286+
return fieldNames.get(column - 1);
287+
}
288+
}
267289
}

0 commit comments

Comments
 (0)