Skip to content

Commit e4068b9

Browse files
committed
DATACOUCH-603 Do not cast query parameters in N1qlQueryCreator.
For IN and NOT_IN - they can take varargs, an array or a JsonArray Don't cast query criteria parameters, let parameter accessor handle that. Fixed conversion of query criteria values to parameters Cleaned up QueryCriteria Co-authored-by: mikereiche <[email protected]>
1 parent 8739702 commit e4068b9

File tree

10 files changed

+355
-179
lines changed

10 files changed

+355
-179
lines changed

src/main/java/org/springframework/data/couchbase/core/query/Query.java

+7-11
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.couchbase.client.java.query.QueryOptions;
2727
import com.couchbase.client.java.query.QueryScanConsistency;
2828
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
29+
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
2930
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
3031
import org.springframework.data.couchbase.repository.query.StringBasedN1qlQueryParser;
3132
import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation;
@@ -189,7 +190,7 @@ public void appendSort(final StringBuilder sb) {
189190
sb.deleteCharAt(sb.length() - 1);
190191
}
191192

192-
public void appendWhere(final StringBuilder sb, int[] paramIndexPtr) {
193+
public void appendWhere(final StringBuilder sb, int[] paramIndexPtr, CouchbaseConverter converter) {
193194
if (!criteria.isEmpty()) {
194195
appendWhereOrAnd(sb);
195196
boolean first = true;
@@ -199,16 +200,11 @@ public void appendWhere(final StringBuilder sb, int[] paramIndexPtr) {
199200
} else {
200201
sb.append(" AND ");
201202
}
202-
sb.append(c.export(paramIndexPtr));
203+
sb.append(c.export(paramIndexPtr, parameters, converter));
203204
}
204205
}
205206
}
206207

207-
public void appendCriteria(StringBuilder sb, QueryCriteria criteria) {
208-
appendWhereOrAnd(sb);
209-
sb.append(criteria.export());
210-
}
211-
212208
public void appendWhereString(StringBuilder sb, String whereString) {
213209
appendWhereOrAnd(sb);
214210
sb.append(whereString);
@@ -257,9 +253,9 @@ private static boolean notQuoted(int start, int end, String querySoFar) {
257253
return true; // is not quoted
258254
}
259255

260-
public String export() {
256+
public String export(int[]... paramIndexPtrHolder) { // used only by tests
261257
StringBuilder sb = new StringBuilder();
262-
appendWhere(sb, null);
258+
appendWhere(sb, paramIndexPtrHolder.length > 0 ? paramIndexPtrHolder[0] : null, null);
263259
appendSort(sb);
264260
appendSkipAndLimit(sb);
265261
return sb.toString();
@@ -270,7 +266,7 @@ public String toN1qlSelectString(ReactiveCouchbaseTemplate template, Class domai
270266
final StringBuilder statement = new StringBuilder();
271267
appendString(statement, n1ql.selectEntity); // select ...
272268
appendWhereString(statement, n1ql.filter); // typeKey = typeValue
273-
appendWhere(statement, new int[] { 0 }); // criteria on this Query
269+
appendWhere(statement, new int[] { 0 }, template.getConverter()); // criteria on this Query
274270
appendSort(statement);
275271
appendSkipAndLimit(statement);
276272
return statement.toString();
@@ -281,7 +277,7 @@ public String toN1qlRemoveString(ReactiveCouchbaseTemplate template, Class domai
281277
final StringBuilder statement = new StringBuilder();
282278
appendString(statement, n1ql.delete); // delete ...
283279
appendWhereString(statement, n1ql.filter); // typeKey = typeValue
284-
appendWhere(statement, null); // criteria on this Query
280+
appendWhere(statement, null, template.getConverter()); // criteria on this Query
285281
appendString(statement, n1ql.returning);
286282
return statement.toString();
287283
}

src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java

+123-36
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
import java.util.LinkedList;
2121
import java.util.List;
2222

23+
import com.couchbase.client.core.error.InvalidArgumentException;
24+
import com.couchbase.client.java.json.JsonArray;
25+
import com.couchbase.client.java.json.JsonObject;
26+
import com.couchbase.client.java.json.JsonValue;
27+
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
2328
import org.springframework.lang.Nullable;
2429

2530
/**
@@ -60,6 +65,10 @@ public class QueryCriteria implements QueryCriteriaDefinition {
6065
this.format = format;
6166
}
6267

68+
Object[] getValue() {
69+
return value;
70+
}
71+
6372
/**
6473
* Static factory method to create a Criteria using the provided key.
6574
*/
@@ -68,8 +77,8 @@ public static QueryCriteria where(String key) {
6877
}
6978

7079
private static QueryCriteria wrap(QueryCriteria criteria) {
71-
QueryCriteria qc = new QueryCriteria(new LinkedList<QueryCriteria>(), criteria.key, criteria.value, null,
72-
criteria.operator, criteria.format);
80+
QueryCriteria qc = new QueryCriteria(new LinkedList<>(), criteria.key, criteria.value, null, criteria.operator,
81+
criteria.format);
7382
return qc;
7483
}
7584

@@ -167,7 +176,7 @@ public QueryCriteria containing(@Nullable Object o) {
167176
public QueryCriteria notContaining(@Nullable Object o) {
168177
value = new QueryCriteria[] { wrap(containing(o)) };
169178
operator = "NOT";
170-
format = format = "not( %3$s )";
179+
format = "not( %3$s )";
171180
return this;
172181
}
173182

@@ -196,7 +205,7 @@ public QueryCriteria isNotNull() {
196205
operator = "IS_NOT_NULL";
197206
value = null;
198207
format = "%1$s is not null";
199-
return (QueryCriteria) this;
208+
return this;
200209
}
201210

202211
public QueryCriteria isMissing() {
@@ -210,7 +219,7 @@ public QueryCriteria isNotMissing() {
210219
operator = "IS_NOT_MiSSING";
211220
value = null;
212221
format = "%1$s is not missing";
213-
return (QueryCriteria) this;
222+
return this;
214223
}
215224

216225
public QueryCriteria isValued() {
@@ -224,68 +233,74 @@ public QueryCriteria isNotValued() {
224233
operator = "IS_NOT_VALUED";
225234
value = null;
226235
format = "%1$s is not valued";
227-
return (QueryCriteria) this;
236+
return this;
228237
}
229238

230239
public QueryCriteria within(@Nullable Object o) {
231240
operator = "WITHIN";
232241
value = new Object[] { o };
233-
format = "%1$s within $3$s";
234-
return (QueryCriteria) this;
242+
format = "%1$s within %3$s";
243+
return this;
235244
}
236245

237246
public QueryCriteria between(@Nullable Object o1, @Nullable Object o2) {
238247
operator = "BETWEEN";
239248
value = new Object[] { o1, o2 };
240249
format = "%1$s between %3$s and %4$s";
241-
return (QueryCriteria) this;
250+
return this;
242251
}
243252

244253
public QueryCriteria in(@Nullable Object... o) {
245254
operator = "IN";
246-
value = o;
247-
StringBuilder sb = new StringBuilder("%1$s in ( [ ");
248-
for (int i = 1; i <= value.length; i++) { // format indices start at 1
249-
if (i > 1)
250-
sb.append(", ");
251-
sb.append("%" + (i + 2) + "$s"); // the first is fieldName, second is operator, args start at 3
255+
format = "%1$s in ( %3$s )";
256+
// IN takes a single argument that is a list
257+
if (o.length > 0) {
258+
if (o[0] instanceof JsonArray || o[0] instanceof List || o[0] instanceof Object[]) {
259+
if (o.length != 1) {
260+
throw new RuntimeException("IN cannot take multiple lists");
261+
}
262+
value = o;
263+
} else {
264+
value = new Object[1];
265+
value[0] = o; // JsonArray.from(o);
266+
}
252267
}
253-
format = sb.append(" ] )").toString();
254-
return (QueryCriteria) this;
268+
return this;
255269
}
256270

257271
public QueryCriteria notIn(@Nullable Object... o) {
258272
value = new QueryCriteria[] { wrap(in(o)) };
259273
operator = "NOT";
260-
format = format = "not( %3$s )"; // field = 1$, operator = 2$, value=$3, $4, ...
261-
return (QueryCriteria) this;
274+
format = "not( %3$s )"; // field = 1$, operator = 2$, value=$3, $4, ...
275+
return this;
262276
}
263277

264278
public QueryCriteria TRUE() { // true/false are reserved, use TRUE/FALSE
265279
value = null;
266280
operator = null;
267-
format = format = "%1$s"; // field = 1$, operator = 2$, value=$3, $4, ...
268-
return (QueryCriteria) this;
281+
format = "%1$s"; // field = 1$, operator = 2$, value=$3, $4, ...
282+
return this;
269283
}
270284

271285
public QueryCriteria FALSE() {
272286
value = new QueryCriteria[] { wrap(TRUE()) };
273287
operator = "not";
274-
format = format = "not( %3$s )";
275-
return (QueryCriteria) this;
288+
format = "not( %3$s )";
289+
return this;
276290
}
277291

278292
/**
279-
* This exports the query criteria into a string to be appended to the beginning of an N1QL statement
293+
* This exports the query criteria chain into a string to be appended to the beginning of an N1QL statement
280294
*
281-
* @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters
282-
* There may already be positional parameters in the beginning of the statement,
283-
* so it may not always start at 1. If it has the value -1, the query is using
284-
* named parameters. If the pointer is null, the query is not using parameters.
295+
* @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters There may
296+
* already be positional parameters in the beginning of the statement, so it may not always start at 1. If it
297+
* has the value -1, the query is using named parameters. If the pointer is null, the query is not using
298+
* parameters.
299+
* @param parameters - parameters of the query. If operands are parameterized, their values are added to parameters
285300
* @return string containing part of N1QL query
286301
*/
287302
@Override
288-
public String export(int[] paramIndexPtr) {
303+
public String export(int[] paramIndexPtr, JsonValue parameters, CouchbaseConverter converter) {
289304
StringBuilder output = new StringBuilder();
290305
boolean first = true;
291306
for (QueryCriteria c : this.criteriaChain) {
@@ -298,7 +313,7 @@ public String export(int[] paramIndexPtr) {
298313
} else {
299314
first = false;
300315
}
301-
c.exportSingle(output, paramIndexPtr);
316+
c.exportSingle(output, paramIndexPtr, parameters, converter);
302317
}
303318

304319
return output.toString();
@@ -310,22 +325,34 @@ public String export(int[] paramIndexPtr) {
310325
* @return string containing part of N1QL query
311326
*/
312327
@Override
313-
public String export() {
314-
return export(null);
328+
public String export() { // used only by tests
329+
return export(null, null, null);
315330

316331
}
317332

318-
private StringBuilder exportSingle(StringBuilder sb, int[] paramIndexPtr) {
333+
/**
334+
* Appends the query criteria to a StringBuilder which will be appended to a N1QL statement
335+
*
336+
* @param sb - the string builder
337+
* @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters There may
338+
* already be positional parameters in the beginning of the statement, so it may not always start at 1. If it
339+
* has the value -1, the query is using named parameters. If the pointer is null, the query is not using
340+
* parameters.
341+
* @param parameters - parameters of the query. If operands are parameterized, their values are added to parameters
342+
* @return string containing part of N1QL query
343+
*/
344+
private StringBuilder exportSingle(StringBuilder sb, int[] paramIndexPtr, JsonValue parameters,
345+
CouchbaseConverter converter) {
319346
String fieldName = maybeQuote(key);
320347
int valueLen = value == null ? 0 : value.length;
321348
Object[] v = new Object[valueLen + 2];
322349
v[0] = fieldName;
323350
v[1] = operator;
324351
for (int i = 0; i < valueLen; i++) {
325352
if (value[i] instanceof QueryCriteria) {
326-
v[i + 2] = "(" + ((QueryCriteria) value[i]).export(paramIndexPtr) + ")";
353+
v[i + 2] = "(" + ((QueryCriteria) value[i]).export(paramIndexPtr, parameters, converter) + ")";
327354
} else {
328-
v[i + 2] = maybeWrapValue(key, value[i], paramIndexPtr);
355+
v[i + 2] = maybeWrapValue(key, value[i], paramIndexPtr, parameters, converter);
329356
}
330357
}
331358

@@ -340,24 +367,84 @@ private StringBuilder exportSingle(StringBuilder sb, int[] paramIndexPtr) {
340367
return sb;
341368
}
342369

343-
private String maybeWrapValue(String key, Object value, int[] paramIndexPtr) {
370+
/**
371+
* Possibly convert an operand to a positional or named parameter
372+
*
373+
* @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters There may
374+
* already be positional parameters in the beginning of the statement, so it may not always start at 1. If it
375+
* has the value -1, the query is using named parameters. If the pointer is null, the query is not using
376+
* parameters.
377+
* @param parameters - parameters of the query. If operands are parameterized, their values are added to parameters
378+
* @return string containing part of N1QL query
379+
*/
380+
private String maybeWrapValue(String key, Object value, int[] paramIndexPtr, JsonValue parameters,
381+
CouchbaseConverter converter) {
344382
if (paramIndexPtr != null) {
345383
if (paramIndexPtr[0] >= 0) {
384+
JsonArray params = (JsonArray) parameters;
385+
// from StringBasedN1qlQueryParser.getPositionalPlaceholderValues()
386+
try {
387+
params.add(convert(converter, value));
388+
} catch (InvalidArgumentException iae) {
389+
if (value instanceof Object[]) {
390+
addAsArray(params, value, converter);
391+
} else {
392+
throw iae;
393+
}
394+
}
346395
return "$" + (++paramIndexPtr[0]); // these are generated in order
347396
} else {
397+
JsonObject params = (JsonObject) parameters;
398+
// from StringBasedN1qlQueryParser.getNamedPlaceholderValues()
399+
try {
400+
params.put(key, convert(converter, value));
401+
} catch (InvalidArgumentException iae) {
402+
if (value instanceof Object[]) {
403+
params.put(key, JsonArray.from((Object[]) value));
404+
} else {
405+
throw iae;
406+
}
407+
}
348408
return "$" + key;
349409
}
350410
}
351411

412+
// Did not convert to a parameter. Add quotes or whatever it might need.
413+
352414
if (value instanceof String) {
353415
return "\"" + value + "\"";
354416
} else if (value == null) {
355417
return "null";
418+
} else if (value instanceof Object[]) { // convert array into sequence of comma-separated values
419+
StringBuffer l = new StringBuffer();
420+
l.append("[");
421+
Object[] array = (Object[]) value;
422+
for (int i = 0; i < array.length; i++) {
423+
if (i > 0) {
424+
l.append(",");
425+
}
426+
l.append(maybeWrapValue(null, array[i], null, null, converter));
427+
}
428+
l.append("]");
429+
return l.toString();
356430
} else {
357431
return value.toString();
358432
}
359433
}
360434

435+
private static Object convert(CouchbaseConverter converter, Object value) {
436+
return converter != null ? converter.convertForWriteIfNeeded(value) : value;
437+
}
438+
439+
private void addAsArray(JsonArray posValues, Object o, CouchbaseConverter converter) {
440+
Object[] array = (Object[]) o;
441+
JsonArray ja = JsonValue.ja();
442+
for (Object e : array) {
443+
ja.add(String.valueOf(convert(converter, e)));
444+
}
445+
posValues.add(ja);
446+
}
447+
361448
private String maybeQuote(String value) {
362449
if (value == null || (value.startsWith("\"") && value.endsWith("\""))) {
363450
return value;

src/main/java/org/springframework/data/couchbase/core/query/QueryCriteriaDefinition.java

+10-5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616
package org.springframework.data.couchbase.core.query;
1717

18+
import com.couchbase.client.java.json.JsonValue;
19+
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
20+
1821
/**
1922
* @author Oliver Gierke
2023
* @author Christoph Strobl
@@ -25,13 +28,15 @@ public interface QueryCriteriaDefinition {
2528
/**
2629
* This exports the query criteria into a string to be appended to the beginning of an N1QL statement
2730
*
28-
* @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters
29-
* There may already be positional parameters in the beginning of the statement,
30-
* so it may not always start at 1. If it has the value -1, the query is using
31-
* named parameters. If the pointer is null, the query is not using parameters.
31+
* @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters There may
32+
* already be positional parameters in the beginning of the statement, so it may not always start at 1. If it
33+
* has the value -1, the query is using named parameters. If the pointer is null, the query is not using
34+
* parameters.
35+
* @param parameters - query parameters. Criteria values that are converted to arguments are added to parameters
36+
* @param converter - converter to use for converting criteria values
3237
* @return string containing part of N1QL query
3338
*/
34-
String export(int[] paramIndexPtr);
39+
String export(int[] paramIndexPtr, JsonValue parameters, CouchbaseConverter converter);
3540

3641
/**
3742
* Export the query criteria to a string without using positional or named parameters.

0 commit comments

Comments
 (0)