Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/main/antlr4/com/aerospike/dsl/Condition.g4
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ operand
| numberOperand
| booleanOperand
| stringOperand
| blobOperand
| listConstant
| orderedMapConstant
| variable
Expand Down Expand Up @@ -134,6 +135,11 @@ stringOperand: QUOTED_STRING;

QUOTED_STRING: ('\'' (~'\'')* '\'') | ('"' (~'"')* '"');

blobOperand: BLOB_LITERAL | B64_LITERAL;

BLOB_LITERAL: [xX] '\'' [0-9a-fA-F]* '\'';
B64_LITERAL: [bB] '64\'' [A-Za-z0-9+/=]* '\'';

// LIST_TYPE_DESIGNATOR is needed here because the lexer tokenizes '[]' as a single token,
// preventing the parser from matching it as '[' ']' for empty list literals.
listConstant: '[' unaryExpression? (',' unaryExpression)* ']' | LIST_TYPE_DESIGNATOR;
Expand All @@ -142,7 +148,7 @@ orderedMapConstant: '{' mapPairConstant? (',' mapPairConstant)* '}';

mapPairConstant: mapKeyOperand ':' unaryExpression;

mapKeyOperand: intOperand | stringOperand;
mapKeyOperand: intOperand | stringOperand | blobOperand;

variable: VARIABLE_REFERENCE;

Expand Down Expand Up @@ -499,6 +505,8 @@ valueIdentifier
| QUOTED_STRING
| signedInt
| IN
| BLOB_LITERAL
| B64_LITERAL
;

valueListIdentifier: valueIdentifier ',' valueIdentifier (',' valueIdentifier)*;
Expand Down
187 changes: 187 additions & 0 deletions src/main/java/com/aerospike/dsl/client/fluent/AerospikeComparator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* Copyright 2012-2026 Aerospike, Inc.
*
* Portions may be licensed to Aerospike, Inc. under one or more contributor
* license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0.
*
* 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.aerospike.dsl.client.fluent;

import com.aerospike.dsl.client.Value;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

/**
* Comparator that orders objects according to the Aerospike server's
* type ordering hierarchy:
* NIL(1) < BOOLEAN(2) < INTEGER(3) < STRING(4) < LIST(5) < MAP(6)
* < BYTES(7) < DOUBLE(8) < GEOJSON(9).
* <p>
* Cross-type comparison is strictly by type ordinal — there is no
* numeric promotion between INTEGER and DOUBLE.
*
* @see <a href="https://aerospike.com/docs/develop/data-types/collections/ordering/">
* Aerospike type ordering</a>
*/
public class AerospikeComparator implements Comparator<Object> {
enum AsType {
NULL (1),
BOOLEAN (2),
INTEGER (3),
STRING (4),
LIST (5),
MAP (6),
BYTES (7),
DOUBLE (8),
GEOJSON (9),
OTHER (10);

private final int value;
AsType(int value) {
this.value = value;
}

public int getOrdinal() {
return value;
}
}

private final boolean caseSensitiveStrings;

public AerospikeComparator() {
this(true);
}

public AerospikeComparator(boolean caseSensitiveStrings) {
this.caseSensitiveStrings = caseSensitiveStrings;
}

private boolean isByteType(Class<?> clazz) {
return Byte.class.equals(clazz) ||
Byte.TYPE.equals(clazz);
}

private boolean isIntegerType(Object o) {
return ((o instanceof Byte) || (o instanceof Character) || (o instanceof Short) || (o instanceof Integer) || (o instanceof Long));
}
private boolean isFloatType(Object o) {
return ((o instanceof Float) || (o instanceof Double));
}

AsType getType(Object o) {
if (o == null) { return AsType.NULL; }
else if (o instanceof Boolean) { return AsType.BOOLEAN; }
else if (isIntegerType(o)) { return AsType.INTEGER; }
else if (o instanceof String) { return AsType.STRING; }
else if (o instanceof List) { return AsType.LIST; }
else if (o instanceof Map) { return AsType.MAP; }
else if (o instanceof Value.HLLValue) { return AsType.BYTES; }
else if (o.getClass().isArray() && isByteType(o.getClass().getComponentType())) { return AsType.BYTES; }
else if (isFloatType(o)) { return AsType.DOUBLE; }
else if (o instanceof Value.GeoJSONValue) { return AsType.GEOJSON; }
else {
return AsType.OTHER;
}
}

private byte[] toByteArray(Object o) {
if (o instanceof Value.HLLValue) {
return ((Value.HLLValue) o).getBytes();
}
return (byte[]) o;
}

private int compareList(List<Object> l1, List<Object> l2) {
int l1Size = l1.size();
int l2Size = l2.size();
for (int index = 0; index < l1Size; index++) {
if (index >= l2Size) {
return 1;
}
int result = compare(l1.get(index), l2.get(index));
if (result != 0) {
return result;
}
}
return l1Size == l2Size ? 0 : -1;
}

private int compareMap(Map<Object, Object> m1, Map<Object, Object> m2) {
if (m1.size() == m2.size()) {
List<Object> sortedKeys1 = new ArrayList<>(m1.keySet());
sortedKeys1.sort(this);
List<Object> sortedKeys2 = new ArrayList<>(m2.keySet());
sortedKeys2.sort(this);
int result = compareList(sortedKeys1, sortedKeys2);
if (result != 0) {
return result;
}
for (int i = 0; i < sortedKeys1.size(); i++) {
Object v1 = m1.get(sortedKeys1.get(i));
Object v2 = m2.get(sortedKeys2.get(i));
result = this.compare(v1, v2);
if (result != 0) {
return result;
}
}
return 0;
}
else {
return m1.size() - m2.size();
}
}

@Override
@SuppressWarnings("unchecked")
public int compare(Object o1, Object o2) {
AsType t1 = getType(o1);
AsType t2 = getType(o2);
if (t1.getOrdinal() != t2.getOrdinal()) {
return t1.getOrdinal() - t2.getOrdinal();
}

switch (t1) {
case NULL:
return 0;
case BOOLEAN:
return Boolean.compare((Boolean)o1, (Boolean)o2);
case INTEGER:
return Long.compare(((Number)o1).longValue(), ((Number)o2).longValue());
case STRING:
if (caseSensitiveStrings) {
return ((String)o1).compareTo((String)o2);
}
else {
return ((String)o1).compareToIgnoreCase((String)o2);
}
case LIST:
return compareList((List<Object>)o1, (List<Object>)o2);
case MAP:
return compareMap((Map<Object, Object>)o1, (Map<Object, Object>)o2);
case BYTES:
return Arrays.compare(toByteArray(o1), toByteArray(o2));
case DOUBLE:
return Double.compare(((Number)o1).doubleValue(), ((Number)o2).doubleValue());
case GEOJSON:
return o1.toString().compareTo(o2.toString());
case OTHER:
default:
throw new UnsupportedOperationException(
"Cannot compare objects of type: " + o1.getClass().getName());
}
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/aerospike/dsl/parts/AbstractPart.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public enum PartType {
EXPRESSION_CONTAINER,
VARIABLE_OPERAND,
PLACEHOLDER_OPERAND,
FUNCTION_ARGS
FUNCTION_ARGS,
BLOB_OPERAND
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import com.aerospike.dsl.client.exp.Exp;
import com.aerospike.dsl.client.exp.ListExp;
import com.aerospike.dsl.parts.path.BasePath;

import com.aerospike.dsl.util.ParsingUtils;

import static com.aerospike.dsl.util.ParsingUtils.objectToExp;
import static com.aerospike.dsl.util.ParsingUtils.parseSignedInt;
import static com.aerospike.dsl.util.ParsingUtils.subtractNullable;

Expand Down Expand Up @@ -60,14 +60,7 @@ public Exp constructExp(BasePath basePath, Exp.Type valueType, int cdtReturnType
cdtReturnType = cdtReturnType | ListReturnType.INVERTED;
}

Exp relativeExp;
if (relative instanceof String rel) {
relativeExp = Exp.val(rel);
} else if (relative instanceof Integer rel) {
relativeExp = Exp.val(rel);
} else {
throw new DslParseException("Unsupported value relative rank");
}
Exp relativeExp = objectToExp(relative);

Exp startExp = Exp.val(start);
if (count == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import com.aerospike.dsl.client.exp.Exp;
import com.aerospike.dsl.client.exp.MapExp;
import com.aerospike.dsl.parts.path.BasePath;

import com.aerospike.dsl.util.ParsingUtils;

import static com.aerospike.dsl.util.ParsingUtils.objectToExp;
import static com.aerospike.dsl.util.ParsingUtils.parseSignedInt;
import static com.aerospike.dsl.util.ParsingUtils.subtractNullable;

Expand Down Expand Up @@ -60,14 +60,7 @@ public Exp constructExp(BasePath basePath, Exp.Type valueType, int cdtReturnType
cdtReturnType = cdtReturnType | MapReturnType.INVERTED;
}

Exp relativeExp;
if (relative instanceof String rel) {
relativeExp = Exp.val(rel);
} else if (relative instanceof Integer rel) {
relativeExp = Exp.val(rel);
} else {
throw new DslParseException("Unsupported value relative rank");
}
Exp relativeExp = objectToExp(relative);

Exp startExp = Exp.val(start);
if (count == null) {
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/aerospike/dsl/parts/operand/BlobOperand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.aerospike.dsl.parts.operand;

import com.aerospike.dsl.client.exp.Exp;
import com.aerospike.dsl.parts.AbstractPart;
import lombok.Getter;

@Getter
public class BlobOperand extends AbstractPart implements ParsedValueOperand {

private final byte[] value;

public BlobOperand(byte[] value) {
super(PartType.BLOB_OPERAND);
this.value = value;
}

@Override
public Exp getExp() {
return Exp.val(value);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.aerospike.dsl.parts.operand;

import com.aerospike.dsl.DslParseException;
import com.aerospike.dsl.client.fluent.AerospikeComparator;
import com.aerospike.dsl.parts.AbstractPart;

import java.util.List;
Expand All @@ -20,6 +21,7 @@
* @see IntOperand
* @see ListOperand
* @see MapOperand
* @see BlobOperand
*/
public interface OperandFactory {

Expand All @@ -34,6 +36,7 @@ public interface OperandFactory {
* <li>{@link Integer} or {@link Long} to {@link IntOperand}.</li>
* <li>{@link List} to {@link ListOperand}.</li>
* <li>{@link Map} to {@link MapOperand}.</li>
* <li>{@code byte[]} to {@link BlobOperand}.</li>
* </ul>
*
* @param value The object to be converted into an operand. This cannot be {@code null}.
Expand Down Expand Up @@ -62,12 +65,16 @@ static AbstractPart createOperand(Object value) {
@SuppressWarnings("unchecked")
SortedMap<Object, Object> objectMap = (SortedMap<Object, Object>) sortedMap;
return new MapOperand(objectMap);
} else if (value instanceof byte[] bytes) {
return new BlobOperand(bytes);
} else if (value instanceof Map<?, ?> map) {
try {
@SuppressWarnings("unchecked")
Map<Object, Object> objectMap = (Map<Object, Object>) map;
return new MapOperand(new TreeMap<>(objectMap));
} catch (ClassCastException | NullPointerException e) {
SortedMap<Object, Object> sortedMap = new TreeMap<>(new AerospikeComparator());
sortedMap.putAll(objectMap);
return new MapOperand(sortedMap);
} catch (ClassCastException | NullPointerException | UnsupportedOperationException e) {
throw new DslParseException(
"Map keys must be mutually comparable for operand creation", e);
}
Expand Down
10 changes: 8 additions & 2 deletions src/main/java/com/aerospike/dsl/parts/operand/StringOperand.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.aerospike.dsl.parts.operand;

import com.aerospike.dsl.DslParseException;
import com.aerospike.dsl.client.exp.Exp;
import com.aerospike.dsl.parts.AbstractPart;
import lombok.Getter;
Expand All @@ -22,8 +23,13 @@ public StringOperand(String string) {
@Override
public Exp getExp() {
if (isBlob) {
byte[] byteValue = Base64.getDecoder().decode(value);
return Exp.val(byteValue);
try {
byte[] byteValue = Base64.getDecoder().decode(value);
return Exp.val(byteValue);
} catch (IllegalArgumentException e) {
throw new DslParseException(
"String compared to BLOB-typed path is not valid Base64: " + value, e);
}
}
return Exp.val(value);
}
Expand Down
Loading
Loading