Skip to content

Commit 566913b

Browse files
authored
Add validation to Insert statement parsing to match the column names with supplied values (#3070)
* fix INSERT statement cases * address comments * address comments * address comments * address comments
1 parent c62dccb commit 566913b

File tree

10 files changed

+224
-76
lines changed

10 files changed

+224
-76
lines changed

docs/ReleaseNotes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Users performing online updates are encouraged to update from [4.0.559.4](#40559
2222
* **Bug fix** Fix 1 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
2323
* **Bug fix** Fix 2 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
2424
* **Bug fix** Fix 3 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
25-
* **Bug fix** Fix 4 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
25+
* **Bug fix** Insert statement does not fully validate the column names with supplied values [(Issue #3069)](https://github.com/FoundationDB/fdb-record-layer/issues/3069)
2626
* **Bug fix** Fix 5 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
2727
* **Performance** Improvement 1 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)
2828
* **Performance** Improvement 2 [(Issue #NNN)](https://github.com/FoundationDB/fdb-record-layer/issues/NNN)

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

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -21,34 +21,27 @@
2121
package com.apple.foundationdb.relational.recordlayer.query;
2222

2323
import com.apple.foundationdb.annotation.API;
24-
2524
import com.apple.foundationdb.record.query.plan.cascades.AliasMap;
2625
import com.apple.foundationdb.record.query.plan.cascades.Column;
2726
import com.apple.foundationdb.record.query.plan.cascades.CorrelationIdentifier;
2827
import com.apple.foundationdb.record.query.plan.cascades.predicates.QueryPredicate;
29-
import com.apple.foundationdb.record.query.plan.cascades.typing.Type;
30-
import com.apple.foundationdb.record.query.plan.cascades.values.AbstractArrayConstructorValue;
3128
import com.apple.foundationdb.record.query.plan.cascades.values.AggregateValue;
3229
import com.apple.foundationdb.record.query.plan.cascades.values.AndOrValue;
3330
import com.apple.foundationdb.record.query.plan.cascades.values.ArithmeticValue;
3431
import com.apple.foundationdb.record.query.plan.cascades.values.BooleanValue;
3532
import com.apple.foundationdb.record.query.plan.cascades.values.ConstantObjectValue;
3633
import com.apple.foundationdb.record.query.plan.cascades.values.LiteralValue;
3734
import com.apple.foundationdb.record.query.plan.cascades.values.NotValue;
38-
import com.apple.foundationdb.record.query.plan.cascades.values.NullValue;
3935
import com.apple.foundationdb.record.query.plan.cascades.values.RecordConstructorValue;
4036
import com.apple.foundationdb.record.query.plan.cascades.values.RelOpValue;
4137
import com.apple.foundationdb.record.query.plan.cascades.values.Value;
42-
import com.apple.foundationdb.relational.api.exceptions.ErrorCode;
4338
import com.apple.foundationdb.relational.api.metadata.DataType;
4439
import com.apple.foundationdb.relational.recordlayer.metadata.DataTypeUtils;
4540
import com.apple.foundationdb.relational.util.Assert;
46-
4741
import com.google.common.base.Suppliers;
4842
import com.google.common.base.Verify;
4943
import com.google.common.collect.ImmutableList;
5044
import com.google.common.collect.Streams;
51-
import com.google.protobuf.ByteString;
5245

5346
import javax.annotation.Nonnull;
5447
import java.util.Collection;
@@ -305,53 +298,5 @@ public static QueryPredicate toUnderlyingPredicate(@Nonnull Expression expressio
305298
return result.get();
306299
}
307300
}
308-
309-
@Nonnull
310-
public static Expression resolveDefaultValue(@Nonnull final Type type) {
311-
// TODO use metadata default values -- for now just do this:
312-
//
313-
// resolution rules:
314-
// - type is nullable ==> null
315-
// - type is not nullable
316-
// - type is of an array type ==> empty array
317-
// - type is a numeric type ==> 0 element
318-
// - type is string ==> ''
319-
// - type is a boolean ==> false
320-
// - type is bytes ==> zero length byte array
321-
// - type is record or enum ==> error
322-
if (type.isNullable()) {
323-
return Expression.fromUnderlying(new NullValue(type));
324-
} else {
325-
switch (type.getTypeCode()) {
326-
case UNKNOWN:
327-
case ANY:
328-
case NULL:
329-
throw Assert.failUnchecked("internal typing error; target type is not properly resolved");
330-
case BOOLEAN:
331-
return Expression.fromUnderlying(LiteralValue.ofScalar(false));
332-
case BYTES:
333-
return Expression.fromUnderlying(LiteralValue.ofScalar(ByteString.empty()));
334-
case DOUBLE:
335-
return Expression.fromUnderlying(LiteralValue.ofScalar(0.0d));
336-
case FLOAT:
337-
return Expression.fromUnderlying(LiteralValue.ofScalar(0.0f));
338-
case INT:
339-
return Expression.fromUnderlying(LiteralValue.ofScalar(0));
340-
case LONG:
341-
return Expression.fromUnderlying(LiteralValue.ofScalar(0L));
342-
case STRING:
343-
return Expression.fromUnderlying(LiteralValue.ofScalar(""));
344-
case ENUM:
345-
throw Assert.failUnchecked(ErrorCode.CANNOT_CONVERT_TYPE, "non-nullable enums must be specified");
346-
case RECORD:
347-
throw Assert.failUnchecked(ErrorCode.CANNOT_CONVERT_TYPE, "non-nullable records must be specified");
348-
case ARRAY:
349-
final var elementType = Assert.notNullUnchecked(((Type.Array) type).getElementType());
350-
return Expression.fromUnderlying(AbstractArrayConstructorValue.LightArrayConstructorValue.emptyArray(elementType));
351-
default:
352-
throw Assert.failUnchecked("unsupported type");
353-
}
354-
}
355-
}
356301
}
357302
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ public RecordLayerColumn visitColumnDefinition(@Nonnull RelationalParser.ColumnD
118118
final var columnId = visitUid(ctx.colName);
119119
final var isRepeated = ctx.ARRAY() != null;
120120
final var isNullable = ctx.columnConstraint() != null ? (Boolean) ctx.columnConstraint().accept(this) : true;
121+
// TODO: We currently do not support NOT NULL for any type other than ARRAY. This is because there is no way to
122+
// specify not "nullability" at the RecordMetaData level. For ARRAY, specifying that is actually possible
123+
// by means of NullableArrayWrapper. In essence, we don't actually need a wrapper per se for non-array types,
124+
// but a way to represent it in RecordMetadata.
125+
Assert.thatUnchecked(isRepeated || isNullable, ErrorCode.UNSUPPORTED_OPERATION, "NOT NULL is only allowed for ARRAY column type");
121126
containsNullableArray = containsNullableArray || (isRepeated && isNullable);
122127
final var columnTypeId = ctx.columnType().customType != null ? visitUid(ctx.columnType().customType) : Identifier.of(ctx.columnType().getText());
123128
final var semanticAnalyzer = getDelegate().getSemanticAnalyzer();

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -764,15 +764,21 @@ private Expressions parseRecordFieldsUnderReorderings(@Nonnull final List<? exte
764764
final var targetTypeReorderings = ImmutableList.copyOf(Assert.notNullUnchecked(
765765
state.getTargetTypeReorderings().get().getChildrenMap()).keySet());
766766
final var resultColumnsBuilder = ImmutableList.<Expression>builder();
767-
767+
Assert.thatUnchecked(targetTypeReorderings.size() >= providedColumnContexts.size(), ErrorCode.SYNTAX_ERROR, "Too many parameters");
768768
for (final var elementField : elementFields) {
769769
final int index = targetTypeReorderings.indexOf(elementField.getFieldName());
770770
final var fieldType = elementField.getFieldType();
771-
final Expression currentFieldColumns;
772-
if (index >= 0) {
771+
Expression currentFieldColumns = null;
772+
if (index >= 0 && index < providedColumnContexts.size()) {
773773
currentFieldColumns = parseRecordField(providedColumnContexts.get(index), elementField);
774+
} else if (index >= providedColumnContexts.size()) {
775+
// column is declared but the value is not provided
776+
Assert.failUnchecked(ErrorCode.SYNTAX_ERROR, "Value of column \"" + elementField.getFieldName() + "\" is not provided");
774777
} else {
775-
currentFieldColumns = Expression.Utils.resolveDefaultValue(fieldType);
778+
// We do not yet support default values for any types, hence it makes to simply fail if the field type
779+
// expects non-null but no value is provided.
780+
Assert.thatUnchecked(fieldType.isNullable(), ErrorCode.NOT_NULL_VIOLATION, "null value in column \"" + elementField.getFieldName() + "\" violates not-null constraint");
781+
currentFieldColumns = Expression.fromUnderlying(new NullValue(fieldType));
776782
}
777783
resultColumnsBuilder.add(currentFieldColumns);
778784
}

fdb-relational-core/src/test/java/com/apple/foundationdb/relational/api/ddl/DdlStatementParsingTest.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import javax.annotation.Nullable;
6565
import java.net.URI;
6666
import java.sql.SQLException;
67+
import java.sql.Types;
6768
import java.util.ArrayList;
6869
import java.util.List;
6970
import java.util.Map;
@@ -254,6 +255,75 @@ public ConstantAction getCreateSchemaTemplateConstantAction(@Nonnull SchemaTempl
254255
});
255256
}
256257

258+
private static Stream<Arguments> typesMap() {
259+
return Stream.of(
260+
Arguments.of(Types.INTEGER, "INTEGER"),
261+
Arguments.of(Types.BIGINT, "BIGINT"),
262+
Arguments.of(Types.FLOAT, "FLOAT"),
263+
Arguments.of(Types.DOUBLE, "DOUBLE"),
264+
Arguments.of(Types.VARCHAR, "STRING"),
265+
Arguments.of(Types.BOOLEAN, "BOOLEAN"),
266+
Arguments.of(Types.BINARY, "BYTES"),
267+
Arguments.of(Types.STRUCT, "BAZ"),
268+
Arguments.of(Types.ARRAY, "STRING ARRAY")
269+
);
270+
}
271+
272+
@ParameterizedTest
273+
@MethodSource("typesMap")
274+
void columnTypeWithNull(int sqlType, @Nonnull String sqlTypeName) throws Exception {
275+
final String stmt = "CREATE SCHEMA TEMPLATE test_template " +
276+
"CREATE TYPE AS STRUCT baz (a bigint, b bigint) " +
277+
"CREATE TABLE bar (id bigint, foo_field " + sqlTypeName + " null, PRIMARY KEY(id))";
278+
shouldWorkWithInjectedFactory(stmt, new AbstractMetadataOperationsFactory() {
279+
@Nonnull
280+
@Override
281+
public ConstantAction getCreateSchemaTemplateConstantAction(@Nonnull final SchemaTemplate template, @Nonnull final Options templateProperties) {
282+
checkColumnNullability(template, sqlType, true);
283+
return txn -> {
284+
};
285+
}
286+
});
287+
}
288+
289+
@ParameterizedTest
290+
@MethodSource("typesMap")
291+
void columnTypeWithNotNull(int sqlType, @Nonnull String sqlTypeName) throws Exception {
292+
final String stmt = "CREATE SCHEMA TEMPLATE test_template " +
293+
"CREATE TYPE AS STRUCT baz (a bigint, b bigint) " +
294+
"CREATE TABLE bar (id bigint, foo_field " + sqlTypeName + " not null, PRIMARY KEY(id))";
295+
if (sqlType == Types.ARRAY) {
296+
shouldWorkWithInjectedFactory(stmt, new AbstractMetadataOperationsFactory() {
297+
@Nonnull
298+
@Override
299+
public ConstantAction getCreateSchemaTemplateConstantAction(@Nonnull final SchemaTemplate template, @Nonnull final Options templateProperties) {
300+
checkColumnNullability(template, sqlType, false);
301+
return txn -> {
302+
};
303+
}
304+
});
305+
} else {
306+
shouldFailWith(stmt, ErrorCode.UNSUPPORTED_OPERATION);
307+
}
308+
}
309+
310+
private static void checkColumnNullability(@Nonnull final SchemaTemplate template, int sqlType, boolean isNullable) {
311+
Assertions.assertInstanceOf(RecordLayerSchemaTemplate.class, template);
312+
Assertions.assertEquals(1, ((RecordLayerSchemaTemplate) template).getTables().size(), "should have only 1 table");
313+
final var table = ((RecordLayerSchemaTemplate) template).findTableByName("BAR");
314+
Assertions.assertTrue(table.isPresent());
315+
final var columns = table.get().getColumns();
316+
Assertions.assertEquals(2, columns.size());
317+
final var maybeNullableArrayColumn = columns.stream().filter(c -> c.getName().equals("FOO_FIELD")).findFirst();
318+
Assertions.assertTrue(maybeNullableArrayColumn.isPresent());
319+
if (isNullable) {
320+
Assertions.assertTrue(maybeNullableArrayColumn.get().getDatatype().isNullable());
321+
} else {
322+
Assertions.assertFalse(maybeNullableArrayColumn.get().getDatatype().isNullable());
323+
}
324+
Assertions.assertEquals(sqlType, maybeNullableArrayColumn.get().getDatatype().getJdbcSqlCode());
325+
}
326+
257327
@Test
258328
void failsToParseEmptyTemplateStatements() throws Exception {
259329
//empty template statements are invalid, and can be rejected in the parser

fdb-relational-core/src/test/java/com/apple/foundationdb/relational/recordlayer/InsertTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public class InsertTest {
5959
public final SimpleDatabaseRule database = new SimpleDatabaseRule(relationalExtension, InsertTest.class, TestSchemas.restaurant());
6060

6161
@Test
62-
void canInsertWithMultipleRecordTypes() throws RelationalException, SQLException {
62+
void canInsertWithMultipleRecordTypes() throws SQLException {
6363
/*
6464
* We want to make sure that we don't accidentally pick up data from different tables
6565
*/
@@ -284,7 +284,7 @@ void canInsertNullableArray() throws SQLException, RelationalException {
284284
}
285285

286286
@Test
287-
void replaceOnInsert() throws SQLException, RelationalException {
287+
void replaceOnInsert() throws SQLException {
288288
try (RelationalConnection conn = DriverManager.getConnection(database.getConnectionUri().toString()).unwrap(RelationalConnection.class)) {
289289
conn.setSchema("TEST_SCHEMA");
290290
try (RelationalStatement s = conn.createStatement()) {

0 commit comments

Comments
 (0)