Skip to content

Commit c86051d

Browse files
authored
Support SQL array subscript operator (#3586)
This introduces support to SQL array subscription operator, with the following caveats: - It works with one-dimensional arrays only. - Similar to other vendors, such as Postgres and Oracle, it returns `null` when the index is out-of-bound instead of raising an error. - Uses 1-based index, as defined in SQL standard. Example: ```sql create table A(pk integer, x integer array, primary key(pk)) insert into A values (1, [1, 2, 3]), (2, [2, 3, 4]), (3, [3, 4, 5]) select X[2] from A where pk = 1; -- returns 2 select X[1+1] from A where pk = 1; -- returns 2 select X[-1] from A where pk = 1; -- returns null select X[1000] from A where pk = 1; -- returns null ``` This also restructures the parsing rules for expressions such that it prohibits more invalid combinations of binary expressions, and aligns them to work well with the subscript operator, and adds support to `not in` which was previously throwing a syntax error. This fixes #3584, #3570, and partially #3588 with few limitations.
1 parent 8e1f3e6 commit c86051d

File tree

16 files changed

+580
-227
lines changed

16 files changed

+580
-227
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
* SubscriptValue.java
3+
*
4+
* This source file is part of the FoundationDB open source project
5+
*
6+
* Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
package com.apple.foundationdb.record.query.plan.cascades.values;
22+
23+
import com.apple.foundationdb.record.EvaluationContext;
24+
import com.apple.foundationdb.record.ObjectPlanHash;
25+
import com.apple.foundationdb.record.PlanDeserializer;
26+
import com.apple.foundationdb.record.PlanHashable;
27+
import com.apple.foundationdb.record.PlanSerializationContext;
28+
import com.apple.foundationdb.record.planprotos.PSubscriptValue;
29+
import com.apple.foundationdb.record.planprotos.PValue;
30+
import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStoreBase;
31+
import com.apple.foundationdb.record.query.plan.cascades.BuiltInFunction;
32+
import com.apple.foundationdb.record.query.plan.cascades.SemanticException;
33+
import com.apple.foundationdb.record.query.plan.cascades.typing.Type;
34+
import com.apple.foundationdb.record.query.plan.cascades.typing.Typed;
35+
import com.apple.foundationdb.record.query.plan.explain.ExplainTokensWithPrecedence;
36+
import com.google.auto.service.AutoService;
37+
import com.google.common.base.Verify;
38+
import com.google.common.collect.Iterables;
39+
import com.google.protobuf.Message;
40+
41+
import javax.annotation.Nonnull;
42+
import javax.annotation.Nullable;
43+
import java.util.List;
44+
import java.util.function.Supplier;
45+
46+
public class SubscriptValue extends AbstractValue {
47+
private static final ObjectPlanHash BASE_HASH = new ObjectPlanHash("Subscript-Value");
48+
49+
@Nonnull
50+
private final Value indexValue;
51+
52+
@Nonnull
53+
private final Value sourceValue;
54+
55+
@Nonnull
56+
private final Type type;
57+
58+
public SubscriptValue(@Nonnull final Value indexValue,
59+
@Nonnull final Value sourceValue) {
60+
this.indexValue = indexValue;
61+
this.sourceValue = sourceValue;
62+
this.type = Verify.verifyNotNull(((Type.Array)sourceValue.getResultType()).getElementType()).nullable();
63+
}
64+
65+
@Nonnull
66+
@Override
67+
protected Iterable<? extends Value> computeChildren() {
68+
return List.of(indexValue, sourceValue);
69+
}
70+
71+
@Nonnull
72+
@Override
73+
public ExplainTokensWithPrecedence explain(@Nonnull final Iterable<Supplier<ExplainTokensWithPrecedence>> explainSuppliers) {
74+
final var indexValueExplain = Iterables.get(explainSuppliers, 0).get();
75+
final var sourceValueExplain = Iterables.get(explainSuppliers, 1).get();
76+
return ExplainTokensWithPrecedence.of(sourceValueExplain.getExplainTokens()
77+
.addOpeningSquareBracket().addOptionalWhitespace()
78+
.addNested(indexValueExplain.getExplainTokens())
79+
.addClosingAngledBracket());
80+
}
81+
82+
@Nullable
83+
@Override
84+
public <M extends Message> Object eval(@Nullable final FDBRecordStoreBase<M> store, @Nonnull final EvaluationContext context) {
85+
final var index = indexValue.eval(store, context);
86+
if (index == null) {
87+
return null;
88+
}
89+
final var source = sourceValue.eval(store, context);
90+
if (source == null) {
91+
return null;
92+
}
93+
// index is 1-based as defined in SQL standard (Foundation, Section: 4.10.2):
94+
// > An array is a collection A in which each element is associated with exactly one ordinal position in A.
95+
// If n is the cardinality of A, then the ordinal position p of an element is an integer in the range 1 (one)
96+
// ≤ p ≤ n.
97+
final var sourceAsList = (List<?>)source;
98+
final var adjustedIndex = (int)index - 1;
99+
if (adjustedIndex < 0 || adjustedIndex >= sourceAsList.size()) {
100+
// this does not raise out-of-bound error.
101+
return null;
102+
}
103+
return sourceAsList.get(adjustedIndex);
104+
}
105+
106+
@Nonnull
107+
@Override
108+
public Type getResultType() {
109+
return type;
110+
}
111+
112+
@Override
113+
public int hashCodeWithoutChildren() {
114+
return PlanHashable.objectsPlanHash(PlanHashable.CURRENT_FOR_CONTINUATION, BASE_HASH);
115+
}
116+
117+
@Nonnull
118+
@Override
119+
public PValue toValueProto(@Nonnull final PlanSerializationContext serializationContext) {
120+
return PValue.newBuilder()
121+
.setSubscriptValue(toProto(serializationContext))
122+
.build();
123+
}
124+
125+
@Override
126+
public int planHash(@Nonnull final PlanHashMode hashMode) {
127+
return PlanHashable.objectsPlanHash(hashMode, BASE_HASH);
128+
}
129+
130+
@Nonnull
131+
@Override
132+
public PSubscriptValue toProto(@Nonnull final PlanSerializationContext serializationContext) {
133+
return PSubscriptValue.newBuilder()
134+
.setIndex(indexValue.toValueProto(serializationContext))
135+
.setSource(sourceValue.toValueProto(serializationContext))
136+
.build();
137+
}
138+
139+
@Nonnull
140+
public static SubscriptValue fromProto(@Nonnull final PlanSerializationContext serializationContext,
141+
@Nonnull final PSubscriptValue subscriptValueProto) {
142+
return new SubscriptValue(Value.fromValueProto(serializationContext, subscriptValueProto.getIndex()),
143+
Value.fromValueProto(serializationContext, subscriptValueProto.getSource()));
144+
}
145+
146+
@Nonnull
147+
@Override
148+
@SuppressWarnings("PMD.CompareObjectsWithEquals")
149+
public Value withChildren(final Iterable<? extends Value> newChildren) {
150+
Verify.verify(Iterables.size(newChildren) == 2);
151+
final var newIndexValue = Iterables.get(newChildren, 0);
152+
final var newSourceValue = Iterables.get(newChildren, 1);
153+
if (indexValue == newIndexValue && newSourceValue == sourceValue) {
154+
return this;
155+
}
156+
return new SubscriptValue(newIndexValue, newSourceValue);
157+
}
158+
159+
/**
160+
* Deserializer.
161+
*/
162+
@AutoService(PlanDeserializer.class)
163+
public static class Deserializer implements PlanDeserializer<PSubscriptValue, SubscriptValue> {
164+
@Nonnull
165+
@Override
166+
public Class<PSubscriptValue> getProtoMessageClass() {
167+
return PSubscriptValue.class;
168+
}
169+
170+
@Nonnull
171+
@Override
172+
public SubscriptValue fromProto(@Nonnull final PlanSerializationContext serializationContext,
173+
@Nonnull final PSubscriptValue subscriptValueProto) {
174+
return SubscriptValue.fromProto(serializationContext, subscriptValueProto);
175+
}
176+
}
177+
178+
/**
179+
* The {@code subscript} function.
180+
*/
181+
@AutoService(BuiltInFunction.class)
182+
public static class SubscriptValueFn extends BuiltInFunction<Value> {
183+
public SubscriptValueFn() {
184+
super("subscript", List.of(Type.primitiveType(Type.TypeCode.INT), Type.any()), Type.any(), SubscriptValue.SubscriptValueFn::encapsulate);
185+
}
186+
187+
@SuppressWarnings({"PMD.UnusedFormalParameter", "PMD.UnusedPrivateMethod"}) // false positive, method is used
188+
private static Value encapsulate(@Nonnull BuiltInFunction<Value> ignored,
189+
@Nonnull final List<? extends Typed> arguments) {
190+
Verify.verify(arguments.size() == 2);
191+
var indexValue = (Value)arguments.get(0);
192+
final var indexMaxType = Type.maximumType(indexValue.getResultType(), Type.primitiveType(Type.TypeCode.INT));
193+
SemanticException.check(indexMaxType != null, SemanticException.ErrorCode.INCOMPATIBLE_TYPE);
194+
indexValue = PromoteValue.inject(indexValue, indexMaxType);
195+
196+
var sourceValue = (Value)arguments.get(1);
197+
Verify.verify(sourceValue.getResultType().isArray());
198+
199+
return new SubscriptValue(indexValue, sourceValue);
200+
}
201+
}
202+
}

fdb-record-layer-core/src/main/proto/record_query_plan.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ message PValue {
255255
PFirstOrDefaultStreamingValue first_or_default_streaming_value = 49;
256256
PEvaluatesToValue evaluates_to_value = 50;
257257
PArrayDistinctValue array_distinct_value = 51;
258+
PSubscriptValue subscript_value = 52;
258259
}
259260
}
260261

@@ -457,6 +458,11 @@ message PEvaluatesToValue {
457458
optional PEvaluation evaluation = 2;
458459
}
459460

461+
message PSubscriptValue {
462+
optional PValue index = 1;
463+
optional PValue source = 2;
464+
}
465+
460466
message PFieldValue {
461467
optional PValue child_value = 1;
462468
optional PFieldPath field_path = 2;

fdb-relational-core/src/main/antlr/RelationalParser.g4

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,7 @@ ofTypeClause
880880
;
881881

882882
arrayConstructor
883-
: LEFT_SQUARE_BRACKET expression (',' expression)* RIGHT_SQUARE_BRACKET
883+
: LEFT_SQUARE_BRACKET expressions? RIGHT_SQUARE_BRACKET
884884
;
885885

886886
userVariables
@@ -1129,43 +1129,38 @@ namedFunctionArg
11291129

11301130
// Expressions, predicates
11311131

1132-
// Simplified approach for expression
11331132
expression
1134-
: notOperator=(NOT | '!') expression #notExpression // done
1135-
| expression logicalOperator expression #logicalExpression // done
1136-
| predicate IS NOT? testValue=(TRUE | FALSE | NULL_LITERAL) #isExpression // done
1137-
| predicate NOT? LIKE pattern=STRING_LITERAL (ESCAPE escape=STRING_LITERAL)? #likePredicate // done
1138-
| predicate #predicateExpression // done
1133+
: notOperator=(NOT | '!') expression #notExpression // done
1134+
| EXISTS '(' query ')' #existsExpressionAtom // done
1135+
| expressionAtom predicate? #predicatedExpression
1136+
| expression logicalOperator expression #logicalExpression // done
11391137
;
11401138

11411139
predicate
1142-
: expressionAtom IN inList #inPredicate // done
1143-
| left=predicate comparisonOperator right=predicate #binaryComparisonPredicate // done
1144-
| operand=predicate NOT? BETWEEN left=predicate AND right=predicate #betweenComparisonPredicate // done
1145-
| expressionAtom #expressionAtomPredicate // done
1140+
: NOT? BETWEEN left=expressionAtom AND right=expressionAtom #betweenComparisonPredicate // done
1141+
| NOT? IN inList #inPredicate // done
1142+
| NOT? LIKE pattern=STRING_LITERAL (ESCAPE escape=STRING_LITERAL)? #likePredicate // done
1143+
| IS NOT? testValue=(TRUE | FALSE | NULL_LITERAL) #isExpression // done
1144+
;
1145+
1146+
expressionAtom
1147+
: constant #constantExpressionAtom // done
1148+
| fullColumnName #fullColumnNameExpressionAtom // done
1149+
| functionCall #functionCallExpressionAtom // done
1150+
| preparedStatementParameter #preparedStatementParameterAtom // done
1151+
| recordConstructor #recordConstructorExpressionAtom // done
1152+
| arrayConstructor #arrayConstructorExpressionAtom // done
1153+
| base=expressionAtom LEFT_SQUARE_BRACKET index=expressionAtom RIGHT_SQUARE_BRACKET #subscriptExpression // done
1154+
| left=expressionAtom bitOperator right=expressionAtom #bitExpressionAtom // done
1155+
| left=expressionAtom mathOperator right=expressionAtom #mathExpressionAtom // done
1156+
| left=expressionAtom comparisonOperator right=expressionAtom #binaryComparisonPredicate // done
11461157
;
11471158

11481159
inList
11491160
: '(' (queryExpressionBody | expressions) ')'
11501161
| preparedStatementParameter
11511162
;
11521163

1153-
// Add in ASTVisitor nullNotnull in constant
1154-
expressionAtom
1155-
: constant #constantExpressionAtom // done
1156-
| fullColumnName #fullColumnNameExpressionAtom // done
1157-
| functionCall #functionCallExpressionAtom // done
1158-
| preparedStatementParameter #preparedStatementParameterAtom // done
1159-
| recordConstructor #recordConstructorExpressionAtom // done
1160-
| arrayConstructor #arrayConstructorExpressionAtom // done
1161-
| EXISTS '(' query ')' #existsExpressionAtom // done
1162-
| '(' queryExpressionBody ')' #subqueryExpressionAtom // done (unsupported)
1163-
| INTERVAL expression intervalType #intervalExpressionAtom // done (unsupported)
1164-
| left=expressionAtom bitOperator right=expressionAtom #bitExpressionAtom // done
1165-
| left=expressionAtom mathOperator right=expressionAtom #mathExpressionAtom // done
1166-
| left=expressionAtom jsonOperator right=expressionAtom #jsonExpressionAtom // done (unsupported)
1167-
;
1168-
11691164
preparedStatementParameter
11701165
: QUESTION
11711166
| NAMED_PARAMETER

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/AstNormalizer.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,6 @@ public Object visitPreparedStatementParameter(@Nonnull RelationalParser.Prepared
446446

447447
@Override
448448
public Object visitInPredicate(@Nonnull RelationalParser.InPredicateContext ctx) {
449-
ctx.expressionAtom().accept(this);
450449
ctx.IN().accept(this);
451450

452451
if (ctx.inList().preparedStatementParameter() != null) {

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/ParseHelpers.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,14 @@ public static String underlineParsingError(@Nonnull Recognizer<?, ?> recognizer,
123123

124124
public static boolean isConstant(@Nonnull final RelationalParser.ExpressionsContext expressionsContext) {
125125
for (final var exp : expressionsContext.expression()) {
126-
if (!(exp instanceof RelationalParser.PredicateExpressionContext)) {
126+
if (!(exp instanceof RelationalParser.PredicatedExpressionContext)) {
127127
return false;
128128
}
129-
final var predicate = (RelationalParser.PredicateExpressionContext) exp;
130-
if (!(predicate.predicate() instanceof RelationalParser.ExpressionAtomPredicateContext)) {
129+
final var predicate = (RelationalParser.PredicatedExpressionContext) exp;
130+
if (predicate.predicate() != null) {
131131
return false;
132132
}
133-
final var expressionAtom = ((RelationalParser.ExpressionAtomPredicateContext) predicate.predicate()).expressionAtom();
133+
final var expressionAtom = predicate.expressionAtom();
134134
if (!(expressionAtom instanceof RelationalParser.ConstantExpressionAtomContext)) {
135135
return false;
136136
}

fdb-relational-core/src/main/java/com/apple/foundationdb/relational/recordlayer/query/functions/SqlFunctionCatalogImpl.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ private static ImmutableMap<String, Function<Integer, Optional<BuiltInFunction<?
122122
.put("&", argumentsCount -> BuiltInFunctionCatalog.resolve("bitand", argumentsCount))
123123
.put("|", argumentsCount -> BuiltInFunctionCatalog.resolve("bitor", argumentsCount))
124124
.put("^", argumentsCount -> BuiltInFunctionCatalog.resolve("bitxor", argumentsCount))
125+
.put("[]", argumentsCount -> BuiltInFunctionCatalog.resolve("subscript", argumentsCount))
125126
.put("bitmap_bit_position", argumentsCount -> BuiltInFunctionCatalog.resolve("bitmap_bit_position", 1 + argumentsCount))
126127
.put("bitmap_bucket_offset", argumentsCount -> BuiltInFunctionCatalog.resolve("bitmap_bucket_offset", 1 + argumentsCount))
127128
.put("bitmap_construct_agg", argumentsCount -> BuiltInFunctionCatalog.resolve("BITMAP_CONSTRUCT_AGG", argumentsCount))

0 commit comments

Comments
 (0)