diff --git a/build.gradle b/build.gradle index e413b648..fe40a01d 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,7 @@ subprojects { googleJavaFormatVersion = '1.7' dockerPluginVersion = '0.34.0' bouncyCastleCryptoVersion = '1.70' + partiqlVersion = '1.2.2' dockerVersion = project.properties['dockerVersion'] ?: project.version } diff --git a/common/src/main/java/com/scalar/dl/genericcontracts/table/v1_0_0/Constants.java b/common/src/main/java/com/scalar/dl/genericcontracts/table/v1_0_0/Constants.java index 793f103f..5c497e65 100644 --- a/common/src/main/java/com/scalar/dl/genericcontracts/table/v1_0_0/Constants.java +++ b/common/src/main/java/com/scalar/dl/genericcontracts/table/v1_0_0/Constants.java @@ -7,6 +7,8 @@ public class Constants { // Metadata public static final String PACKAGE = "table"; public static final String VERSION = "v1_0_0"; + public static final String CONTRACT_CREATE = PACKAGE + "." + VERSION + ".Create"; + public static final String CONTRACT_INSERT = PACKAGE + "." + VERSION + ".Insert"; public static final String CONTRACT_GET_ASSET_ID = PACKAGE + "." + VERSION + ".GetAssetId"; public static final String CONTRACT_SCAN = PACKAGE + "." + VERSION + ".Scan"; diff --git a/settings.gradle b/settings.gradle index f9dd25e1..837da0bc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,3 +4,4 @@ include 'rpc' include 'client' include 'schema-loader' include 'generic-contracts' +include 'table-store' diff --git a/table-store/build.gradle b/table-store/build.gradle new file mode 100644 index 00000000..03134369 --- /dev/null +++ b/table-store/build.gradle @@ -0,0 +1,52 @@ +plugins { + id 'net.ltgt.errorprone' version "${errorpronePluginVersion}" + id "com.github.spotbugs" version "${spotbugsPluginVersion}" +} + +dependencies { + implementation project(':client') + implementation project(':generic-contracts') + implementation group: 'org.partiql', name: 'partiql-parser', version: "${partiqlVersion}" + + // for Error Prone + errorprone "com.google.errorprone:error_prone_core:${errorproneVersion}" + errorproneJavac "com.google.errorprone:javac:${errorproneJavacVersion}" + + // for SpotBugs + spotbugs "com.github.spotbugs:spotbugs:${spotbugsVersion}" + compileOnly "com.github.spotbugs:spotbugs-annotations:${spotbugsVersion}" + testCompileOnly "com.github.spotbugs:spotbugs-annotations:${spotbugsVersion}" +} + +spotless { + java { + target 'src/*/java/**/*.java' + importOrder() + removeUnusedImports() + googleJavaFormat('1.7') + } +} + +spotbugs { + ignoreFailures = false + showStackTraces = true + showProgress = true + effort = 'default' + reportLevel = 'default' + maxHeapSize = '1g' + extraArgs = [ '-nested:false' ] + jvmArgs = [ '-Duser.language=en' ] +} + +spotbugsMain.reports { + html.enabled = true +} + +spotbugsTest.reports { + html.enabled = true +} + +task sourcesJar(type: Jar) { + classifier = 'sources' + from sourceSets.main.allSource +} diff --git a/table-store/src/main/java/com/scalar/dl/tablestore/client/error/TableStoreClientError.java b/table-store/src/main/java/com/scalar/dl/tablestore/client/error/TableStoreClientError.java new file mode 100644 index 00000000..af7cac29 --- /dev/null +++ b/table-store/src/main/java/com/scalar/dl/tablestore/client/error/TableStoreClientError.java @@ -0,0 +1,109 @@ +package com.scalar.dl.tablestore.client.error; + +import com.scalar.dl.ledger.error.ScalarDlError; +import com.scalar.dl.ledger.service.StatusCode; + +public enum TableStoreClientError implements ScalarDlError { + + // + // Errors for INVALID_ARGUMENT(414) + // + SYNTAX_ERROR_IN_PARTIQL_PARSER( + StatusCode.INVALID_ARGUMENT, + "001", + "Syntax error. Line=%d, Offset=%d, Length=%d, Code=%s", + "", + ""), + SYNTAX_ERROR_INVALID_PRIMARY_KEY_SPECIFICATION( + StatusCode.INVALID_ARGUMENT, + "002", + "Syntax error. The primary key column must be specified only once in a table.", + "", + ""), + SYNTAX_ERROR_INVALID_COLUMN_CONSTRAINTS( + StatusCode.INVALID_ARGUMENT, + "003", + "Syntax error. The specified column constraint is invalid.", + "", + ""), + SYNTAX_ERROR_INVALID_DATA_TYPE( + StatusCode.INVALID_ARGUMENT, + "004", + "Syntax error. The specified data type is invalid.", + "", + ""), + SYNTAX_ERROR_INVALID_INSERT_STATEMENT( + StatusCode.INVALID_ARGUMENT, + "005", + "Syntax error. The specified insert statement is invalid.", + "", + ""), + SYNTAX_ERROR_INVALID_STATEMENT( + StatusCode.INVALID_ARGUMENT, + "006", + "Syntax error. The specified statement is invalid.", + "", + ""), + SYNTAX_ERROR_INVALID_EXPRESSION( + StatusCode.INVALID_ARGUMENT, + "007", + "Syntax error. The specified expression is invalid. Expression: %s", + "", + ""), + SYNTAX_ERROR_INVALID_LITERAL( + StatusCode.INVALID_ARGUMENT, + "008", + "Syntax error. The specified literal is invalid. Literal: %s", + "", + ""), + ; + + private static final String COMPONENT_NAME = "DL-TABLE-STORE"; + + private final StatusCode statusCode; + private final String id; + private final String message; + private final String cause; + private final String solution; + + TableStoreClientError( + StatusCode statusCode, String id, String message, String cause, String solution) { + validate(COMPONENT_NAME, statusCode, id, message, cause, solution); + + this.statusCode = statusCode; + this.id = id; + this.message = message; + this.cause = cause; + this.solution = solution; + } + + @Override + public String getComponentName() { + return COMPONENT_NAME; + } + + @Override + public StatusCode getStatusCode() { + return statusCode; + } + + @Override + public String getId() { + return id; + } + + @Override + public String getMessage() { + return message; + } + + @Override + public String getCause() { + return cause; + } + + @Override + public String getSolution() { + return solution; + } +} diff --git a/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/DataType.java b/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/DataType.java new file mode 100644 index 00000000..42e9e206 --- /dev/null +++ b/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/DataType.java @@ -0,0 +1,7 @@ +package com.scalar.dl.tablestore.client.partiql; + +public enum DataType { + BOOLEAN, + NUMBER, + STRING, +} diff --git a/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/parser/PartiqlParserVisitor.java b/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/parser/PartiqlParserVisitor.java new file mode 100644 index 00000000..8e9330f5 --- /dev/null +++ b/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/parser/PartiqlParserVisitor.java @@ -0,0 +1,227 @@ +package com.scalar.dl.tablestore.client.partiql.parser; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.BigIntegerNode; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.DecimalNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.node.ValueNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.scalar.dl.ledger.util.JacksonSerDe; +import com.scalar.dl.tablestore.client.error.TableStoreClientError; +import com.scalar.dl.tablestore.client.partiql.DataType; +import com.scalar.dl.tablestore.client.partiql.statement.ContractStatement; +import com.scalar.dl.tablestore.client.partiql.statement.CreateTableStatement; +import com.scalar.dl.tablestore.client.partiql.statement.InsertStatement; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; +import org.partiql.ast.AstNode; +import org.partiql.ast.AstVisitor; +import org.partiql.ast.Literal; +import org.partiql.ast.ddl.AttributeConstraint; +import org.partiql.ast.ddl.AttributeConstraint.Unique; +import org.partiql.ast.ddl.ColumnDefinition; +import org.partiql.ast.ddl.CreateTable; +import org.partiql.ast.dml.Insert; +import org.partiql.ast.dml.InsertSource; +import org.partiql.ast.expr.Expr; +import org.partiql.ast.expr.ExprArray; +import org.partiql.ast.expr.ExprLit; +import org.partiql.ast.expr.ExprRowValue; +import org.partiql.ast.expr.ExprStruct; +import org.partiql.ast.expr.ExprStruct.Field; +import org.partiql.ast.expr.ExprValues; +import org.partiql.ast.expr.ExprVarRef; +import org.partiql.ast.sql.SqlBlock; +import org.partiql.ast.sql.SqlDialect; +import org.partiql.ast.sql.SqlLayout; + +public class PartiqlParserVisitor extends AstVisitor, Void> { + private final JacksonSerDe jacksonSerDe = new JacksonSerDe(new ObjectMapper()); + + @Override + public List visitCreateTable(CreateTable astNode, Void context) { + String table = astNode.getName().getIdentifier().getText(); + String primaryKey = null; + DataType primaryKeyType = null; + ImmutableMap.Builder indexes = ImmutableMap.builder(); + + for (ColumnDefinition columnDefinition : astNode.getColumns()) { + DataType dataType = extractDataType(columnDefinition.getDataType()); + + List constraints = columnDefinition.getConstraints(); + if (constraints.isEmpty()) { + indexes.put(columnDefinition.getName().getText(), dataType); + continue; + } + + if (constraints.size() == 1 && constraints.get(0) instanceof Unique) { + if (primaryKey != null) { + throw new IllegalArgumentException( + TableStoreClientError.SYNTAX_ERROR_INVALID_PRIMARY_KEY_SPECIFICATION.buildMessage()); + } + primaryKey = columnDefinition.getName().getText(); + primaryKeyType = dataType; + } else { + throw new IllegalArgumentException( + TableStoreClientError.SYNTAX_ERROR_INVALID_COLUMN_CONSTRAINTS.buildMessage()); + } + } + + if (primaryKey == null) { + throw new IllegalArgumentException( + TableStoreClientError.SYNTAX_ERROR_INVALID_PRIMARY_KEY_SPECIFICATION.buildMessage()); + } + + return ImmutableList.of( + CreateTableStatement.create(table, primaryKey, primaryKeyType, indexes.build())); + } + + @Override + public List visitInsert(Insert astNode, Void context) { + InsertSource source = astNode.getSource(); + + if (astNode.getAsAlias() != null || astNode.getOnConflict() != null) { + throw new IllegalArgumentException( + TableStoreClientError.SYNTAX_ERROR_INVALID_INSERT_STATEMENT.buildMessage()); + } + + if (source instanceof InsertSource.FromExpr) { + String table = astNode.getTableName().getIdentifier().getText(); + InsertSource.FromExpr insertExpr = (InsertSource.FromExpr) source; + + if (insertExpr.getColumns() != null) { + throw new IllegalArgumentException( + TableStoreClientError.SYNTAX_ERROR_INVALID_INSERT_STATEMENT.buildMessage()); + } + + if (insertExpr.getExpr() instanceof ExprValues) { + List rowValues = ((ExprValues) insertExpr.getExpr()).getRows(); + if (rowValues.size() != 1 + || !(rowValues.get(0) instanceof ExprRowValue) + || !(((ExprRowValue) rowValues.get(0)).getValues().get(0) instanceof ExprStruct)) { + throw new IllegalArgumentException( + TableStoreClientError.SYNTAX_ERROR_INVALID_INSERT_STATEMENT.buildMessage()); + } + + ExprStruct struct = (ExprStruct) ((ExprRowValue) rowValues.get(0)).getValues().get(0); + return ImmutableList.of( + InsertStatement.create(table, convertExprStructToObjectNode(struct))); + } + } + + throw new IllegalArgumentException( + TableStoreClientError.SYNTAX_ERROR_INVALID_INSERT_STATEMENT.buildMessage()); + } + + @Override + public List defaultVisit(AstNode astNode, Void context) { + throw new IllegalArgumentException( + TableStoreClientError.SYNTAX_ERROR_INVALID_STATEMENT.buildMessage()); + } + + @Override + public List defaultReturn(AstNode astNode, Void context) { + throw new IllegalArgumentException( + TableStoreClientError.SYNTAX_ERROR_INVALID_STATEMENT.buildMessage()); + } + + private DataType extractDataType(org.partiql.ast.DataType dataType) { + switch (dataType.code()) { + case org.partiql.ast.DataType.BOOL: + case org.partiql.ast.DataType.BOOLEAN: + return DataType.BOOLEAN; + case org.partiql.ast.DataType.STRING: + return DataType.STRING; + case org.partiql.ast.DataType.INT: + case org.partiql.ast.DataType.INTEGER: + case org.partiql.ast.DataType.BIGINT: + case org.partiql.ast.DataType.FLOAT: + case org.partiql.ast.DataType.DOUBLE_PRECISION: + return DataType.NUMBER; + default: + throw new IllegalArgumentException( + TableStoreClientError.SYNTAX_ERROR_INVALID_DATA_TYPE.buildMessage(dataType.name())); + } + } + + private String extractNameFromExpr(Expr expr) { + if (expr instanceof ExprVarRef) { + ExprVarRef varRef = (ExprVarRef) expr; + return varRef.getIdentifier().getIdentifier().getText(); + } + + throw new IllegalArgumentException( + TableStoreClientError.SYNTAX_ERROR_INVALID_EXPRESSION.buildMessage(toSql(expr))); + } + + private ValueNode convertExprLitToValueNode(ExprLit exprLit) { + Literal literal = exprLit.getLit(); + switch (literal.code()) { + case Literal.NULL: + return NullNode.getInstance(); + case Literal.BOOL: + return BooleanNode.valueOf(literal.booleanValue()); + case Literal.INT_NUM: + return BigIntegerNode.valueOf(new BigInteger(literal.numberValue())); + case Literal.APPROX_NUM: + case Literal.EXACT_NUM: + return DecimalNode.valueOf(new BigDecimal(literal.numberValue())); + case Literal.STRING: + return TextNode.valueOf(literal.stringValue()); + default: + throw new IllegalArgumentException( + TableStoreClientError.SYNTAX_ERROR_INVALID_LITERAL.buildMessage(toSql(literal))); + } + } + + private ArrayNode convertExprArrayToArrayNode(ExprArray exprArray) { + ArrayNode array = jacksonSerDe.getObjectMapper().createArrayNode(); + for (Expr expr : exprArray.getValues()) { + if (expr instanceof ExprLit) { + array.add(convertExprLitToValueNode((ExprLit) expr)); + } else if (expr instanceof ExprArray) { + array.add(convertExprArrayToArrayNode((ExprArray) expr)); + } else if (expr instanceof ExprStruct) { + array.add(convertExprStructToObjectNode((ExprStruct) expr)); + } else { + throw new IllegalArgumentException( + TableStoreClientError.SYNTAX_ERROR_INVALID_EXPRESSION.buildMessage(toSql(expr))); + } + } + return array; + } + + private ObjectNode convertExprStructToObjectNode(ExprStruct exprStruct) { + ObjectNode object = jacksonSerDe.getObjectMapper().createObjectNode(); + for (Field field : exprStruct.getFields()) { + // We accept both identifier and string for a field name; i.e., both {a: 1} and {"a": 1} are + // accepted as the same. Note that, since any strings, e.g., {"a-b": 1}, are valid in the + // parser, the format of the field name is additionally validated on the contract side, and + // "a-b" will be rejected there. + String name = extractNameFromExpr(field.getName()); + Expr expr = field.getValue(); + if (expr instanceof ExprLit) { + object.set(name, convertExprLitToValueNode((ExprLit) expr)); + } else if (expr instanceof ExprArray) { + object.set(name, convertExprArrayToArrayNode((ExprArray) expr)); + } else if (expr instanceof ExprStruct) { + object.set(name, convertExprStructToObjectNode((ExprStruct) expr)); + } else { + throw new IllegalArgumentException( + TableStoreClientError.SYNTAX_ERROR_INVALID_EXPRESSION.buildMessage(toSql(expr))); + } + } + return object; + } + + private String toSql(AstNode astNode) { + SqlBlock sqlBlock = SqlDialect.getSTANDARD().transform(astNode); + return SqlLayout.getSTANDARD().print(sqlBlock); + } +} diff --git a/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/parser/ScalarPartiqlParser.java b/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/parser/ScalarPartiqlParser.java new file mode 100644 index 00000000..78edc3a9 --- /dev/null +++ b/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/parser/ScalarPartiqlParser.java @@ -0,0 +1,50 @@ +package com.scalar.dl.tablestore.client.partiql.parser; + +import com.scalar.dl.tablestore.client.error.TableStoreClientError; +import com.scalar.dl.tablestore.client.partiql.statement.ContractStatement; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.concurrent.ThreadSafe; +import org.partiql.parser.PartiQLParser; +import org.partiql.spi.Context; +import org.partiql.spi.errors.PErrorKind; +import org.partiql.spi.errors.PErrorListener; +import org.partiql.spi.errors.PRuntimeException; +import org.partiql.spi.errors.Severity; + +@ThreadSafe +public class ScalarPartiqlParser { + + private static final PartiQLParser parser = PartiQLParser.standard(); + private static final PErrorListener errorListener = + error -> { + Throwable cause = error.get("CAUSE", Throwable.class); + if (cause instanceof IllegalArgumentException) { + throw (IllegalArgumentException) cause; + } else if (error.severity.code() == Severity.ERROR + && error.kind.code() == PErrorKind.SYNTAX) { + if (error.location == null) { + throw new IllegalArgumentException( + TableStoreClientError.SYNTAX_ERROR_IN_PARTIQL_PARSER.buildMessage( + null, null, null, error.name())); + } else { + throw new IllegalArgumentException( + TableStoreClientError.SYNTAX_ERROR_IN_PARTIQL_PARSER.buildMessage( + error.location.line, + error.location.offset, + error.location.length, + error.name())); + } + } + throw new PRuntimeException(error); + }; + + private ScalarPartiqlParser() {} + + public static List parse(String sql) { + final PartiqlParserVisitor visitor = new PartiqlParserVisitor(); + return parser.parse(sql, Context.of(errorListener)).statements.stream() + .flatMap(statement -> statement.accept(visitor, null).stream()) + .collect(Collectors.toList()); + } +} diff --git a/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/statement/AbstractJacksonBasedContractStatement.java b/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/statement/AbstractJacksonBasedContractStatement.java new file mode 100644 index 00000000..a48d7876 --- /dev/null +++ b/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/statement/AbstractJacksonBasedContractStatement.java @@ -0,0 +1,9 @@ +package com.scalar.dl.tablestore.client.partiql.statement; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.scalar.dl.ledger.util.JacksonSerDe; + +public abstract class AbstractJacksonBasedContractStatement implements ContractStatement { + + protected static final JacksonSerDe jacksonSerDe = new JacksonSerDe(new ObjectMapper()); +} diff --git a/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/statement/ContractStatement.java b/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/statement/ContractStatement.java new file mode 100644 index 00000000..26a11017 --- /dev/null +++ b/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/statement/ContractStatement.java @@ -0,0 +1,8 @@ +package com.scalar.dl.tablestore.client.partiql.statement; + +public interface ContractStatement { + + String getContractId(); + + String getArguments(); +} diff --git a/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/statement/CreateTableStatement.java b/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/statement/CreateTableStatement.java new file mode 100644 index 00000000..6d83fc36 --- /dev/null +++ b/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/statement/CreateTableStatement.java @@ -0,0 +1,86 @@ +package com.scalar.dl.tablestore.client.partiql.statement; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMap; +import com.scalar.dl.genericcontracts.table.v1_0_0.Constants; +import com.scalar.dl.tablestore.client.partiql.DataType; +import java.util.Objects; + +public class CreateTableStatement extends AbstractJacksonBasedContractStatement { + + private final String contractId; + private final JsonNode arguments; + + private CreateTableStatement(JsonNode arguments) { + this.contractId = Constants.CONTRACT_CREATE; + this.arguments = Objects.requireNonNull(arguments); + } + + @Override + public String getContractId() { + return contractId; + } + + @Override + public String getArguments() { + return jacksonSerDe.serialize(arguments); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("contractId", getContractId()) + .add("arguments", getArguments()) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CreateTableStatement)) { + return false; + } + CreateTableStatement that = (CreateTableStatement) o; + return Objects.equals(arguments, that.arguments); + } + + @Override + public int hashCode() { + return Objects.hash(arguments); + } + + private static JsonNode buildArguments( + String table, + String primaryKey, + DataType primaryKeyType, + ImmutableMap indexes) { + ObjectNode arguments = jacksonSerDe.getObjectMapper().createObjectNode(); + arguments.put(Constants.TABLE_NAME, table); + arguments.put(Constants.TABLE_KEY, primaryKey); + arguments.put(Constants.TABLE_KEY_TYPE, primaryKeyType.name()); + ArrayNode indexArray = jacksonSerDe.getObjectMapper().createArrayNode(); + indexes.forEach( + (indexKey, indexKeyType) -> + indexArray.add( + jacksonSerDe + .getObjectMapper() + .createObjectNode() + .put(Constants.INDEX_KEY, indexKey) + .put(Constants.INDEX_KEY_TYPE, indexKeyType.name()))); + arguments.set(Constants.TABLE_INDEXES, indexArray); + return arguments; + } + + public static CreateTableStatement create( + String table, + String primaryKey, + DataType primaryKeyType, + ImmutableMap indexes) { + return new CreateTableStatement(buildArguments(table, primaryKey, primaryKeyType, indexes)); + } +} diff --git a/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/statement/InsertStatement.java b/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/statement/InsertStatement.java new file mode 100644 index 00000000..cf5a2d3a --- /dev/null +++ b/table-store/src/main/java/com/scalar/dl/tablestore/client/partiql/statement/InsertStatement.java @@ -0,0 +1,64 @@ +package com.scalar.dl.tablestore.client.partiql.statement; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.base.MoreObjects; +import com.scalar.dl.genericcontracts.table.v1_0_0.Constants; +import java.util.Objects; + +public class InsertStatement extends AbstractJacksonBasedContractStatement { + + private final String contractId; + private final JsonNode arguments; + + private InsertStatement(JsonNode arguments) { + this.contractId = Constants.CONTRACT_INSERT; + this.arguments = Objects.requireNonNull(arguments); + } + + @Override + public String getContractId() { + return contractId; + } + + @Override + public String getArguments() { + return jacksonSerDe.serialize(arguments); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("contractId", getContractId()) + .add("arguments", getArguments()) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof InsertStatement)) { + return false; + } + InsertStatement that = (InsertStatement) o; + return Objects.equals(arguments, that.arguments); + } + + @Override + public int hashCode() { + return Objects.hash(arguments); + } + + private static JsonNode buildArguments(String table, JsonNode values) { + ObjectNode arguments = jacksonSerDe.getObjectMapper().createObjectNode(); + arguments.put(Constants.RECORD_TABLE, table); + arguments.set(Constants.RECORD_VALUES, values); + return arguments; + } + + public static InsertStatement create(String table, JsonNode values) { + return new InsertStatement(buildArguments(table, values)); + } +} diff --git a/table-store/src/test/java/com/scalar/dl/tablestore/client/error/TableStoreClientErrorTest.java b/table-store/src/test/java/com/scalar/dl/tablestore/client/error/TableStoreClientErrorTest.java new file mode 100644 index 00000000..a81b7481 --- /dev/null +++ b/table-store/src/test/java/com/scalar/dl/tablestore/client/error/TableStoreClientErrorTest.java @@ -0,0 +1,40 @@ +package com.scalar.dl.tablestore.client.error; + +import java.util.Arrays; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +public class TableStoreClientErrorTest { + + @Test + public void checkDuplicateErrorCode() { + Assertions.assertThat( + Arrays.stream(TableStoreClientError.values()).map(TableStoreClientError::buildCode)) + .doesNotHaveDuplicates(); + } + + @Test + public void buildCode_ShouldBuildCorrectCode() { + // Arrange + TableStoreClientError error = TableStoreClientError.SYNTAX_ERROR_IN_PARTIQL_PARSER; + + // Act + String code = error.buildCode(); + + // Assert + Assertions.assertThat(code).isEqualTo("DL-TABLE-STORE-414001"); + } + + @Test + public void buildMessage_ShouldBuildCorrectMessage() { + // Arrange + TableStoreClientError error = TableStoreClientError.SYNTAX_ERROR_IN_PARTIQL_PARSER; + + // Act + String message = error.buildMessage(1, 1, 1, "code"); + + // Assert + Assertions.assertThat(message) + .isEqualTo("DL-TABLE-STORE-414001: Syntax error. Line=1, Offset=1, Length=1, Code=code"); + } +} diff --git a/table-store/src/test/java/com/scalar/dl/tablestore/client/partiql/parser/PartiqlParserVisitorTest.java b/table-store/src/test/java/com/scalar/dl/tablestore/client/partiql/parser/PartiqlParserVisitorTest.java new file mode 100644 index 00000000..dfad28cf --- /dev/null +++ b/table-store/src/test/java/com/scalar/dl/tablestore/client/partiql/parser/PartiqlParserVisitorTest.java @@ -0,0 +1,204 @@ +package com.scalar.dl.tablestore.client.partiql.parser; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.scalar.dl.tablestore.client.partiql.DataType; +import com.scalar.dl.tablestore.client.partiql.statement.ContractStatement; +import com.scalar.dl.tablestore.client.partiql.statement.CreateTableStatement; +import com.scalar.dl.tablestore.client.partiql.statement.InsertStatement; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class PartiqlParserVisitorTest { + private static final ObjectMapper mapper = new ObjectMapper(); + + @Test + public void parse_CreateTableSqlGiven_ShouldParseCorrectly() { + // Arrange Act + List statements = + ScalarPartiqlParser.parse( + "CREATE TABLE tbl (col1 STRING PRIMARY KEY);" + + "CREATE TABLE tbl (col1 STRING PRIMARY KEY, col2 BOOL, col3 BOOLEAN);" + + "CREATE TABLE tbl (col1 INT PRIMARY KEY, col2 STRING);" + + "CREATE TABLE tbl (col1 BOOLEAN PRIMARY KEY, col2 INT, col3 INTEGER, col4 BIGINT, col5 FLOAT, col6 DOUBLE PRECISION);"); + + // Assert + assertThat(statements.size()).isEqualTo(4); + assertThat(statements.get(0)) + .isEqualTo(CreateTableStatement.create("tbl", "col1", DataType.STRING, ImmutableMap.of())); + assertThat(statements.get(1)) + .isEqualTo( + CreateTableStatement.create( + "tbl", + "col1", + DataType.STRING, + ImmutableMap.of("col2", DataType.BOOLEAN, "col3", DataType.BOOLEAN))); + assertThat(statements.get(2)) + .isEqualTo( + CreateTableStatement.create( + "tbl", "col1", DataType.NUMBER, ImmutableMap.of("col2", DataType.STRING))); + assertThat(statements.get(3)) + .isEqualTo( + CreateTableStatement.create( + "tbl", + "col1", + DataType.BOOLEAN, + ImmutableMap.of( + "col2", + DataType.NUMBER, + "col3", + DataType.NUMBER, + "col4", + DataType.NUMBER, + "col5", + DataType.NUMBER, + "col6", + DataType.NUMBER))); + } + + @Test + public void parse_CreateTableSqlWithoutPrimaryKeyGiven_ShouldThrowIllegalArgumentException() { + // Arrange Act Assert + assertThatThrownBy( + () -> + ScalarPartiqlParser.parse("CREATE TABLE tbl (col1 INT, col2 BOOLEAN, col3 STRING)")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void + parse_CreateTableSqlWithMultiplePrimaryKeysGiven_ShouldThrowIllegalArgumentException() { + // Arrange Act Assert + assertThatThrownBy( + () -> + ScalarPartiqlParser.parse( + "CREATE TABLE tbl (col1 INT PRIMARY KEY, col2 BOOLEAN PRIMARY KEY, col3 STRING)")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void parse_CreateTableSqlWithInvalidConstraintGiven_ShouldThrowIllegalArgumentException() { + // Arrange Act Assert + assertThatThrownBy( + () -> + ScalarPartiqlParser.parse( + "CREATE TABLE tbl (col1 INT PRIMARY KEY, col2 BOOLEAN NOT NULL, col3 STRING)")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void parse_InsertSqlGiven_ShouldParseCorrectly() { + // Arrange + BigInteger one = BigInteger.ONE; + BigInteger two = BigInteger.valueOf(2); + JsonNode array = mapper.createArrayNode().add(one).add(two); + JsonNode object = mapper.createObjectNode().put("x", one).put("y", one); + + // Act + List statements = + ScalarPartiqlParser.parse( + "INSERT INTO tbl VALUES {};" + + "INSERT INTO tbl VALUES {col1: 'aaa', col2: false, col3: 123, col4: 1.23, col5: 1.23e4, col6: null};" + + "INSERT INTO tbl VALUES {col1: 'aaa', col2: [1, [1, 2], {x: 1, y: 1}]};" + + "INSERT INTO tbl VALUES {col1: 'aaa', col2: {col3: [1, 2]}};" + + "INSERT INTO tbl VALUES {col1: 'aaa', col2: {col3: {x: 1, y: 1}}};" + + "INSERT INTO \"tbl\" VALUES {\"col1\": 'aaa'};"); + + // Assert + assertThat(statements.size()).isEqualTo(6); + assertThat(statements.get(0)) + .isEqualTo(InsertStatement.create("tbl", mapper.createObjectNode())); + assertThat(statements.get(1)) + .isEqualTo( + InsertStatement.create( + "tbl", + mapper + .createObjectNode() + .put("col1", "aaa") + .put("col2", false) + .put("col3", new BigInteger("123")) + .put("col4", new BigDecimal("1.23")) + .put("col5", new BigDecimal("1.23e4")) + .set("col6", null))); + assertThat(statements.get(2)) + .isEqualTo( + InsertStatement.create( + "tbl", + mapper + .createObjectNode() + .put("col1", "aaa") + .set("col2", mapper.createArrayNode().add(one).add(array).add(object)))); + assertThat(statements.get(3)) + .isEqualTo( + InsertStatement.create( + "tbl", + mapper + .createObjectNode() + .put("col1", "aaa") + .set("col2", mapper.createObjectNode().set("col3", array)))); + assertThat(statements.get(4)) + .isEqualTo( + InsertStatement.create( + "tbl", + mapper + .createObjectNode() + .put("col1", "aaa") + .set("col2", mapper.createObjectNode().set("col3", object)))); + assertThat(statements.get(5)) + .isEqualTo(InsertStatement.create("tbl", mapper.createObjectNode().put("col1", "aaa"))); + } + + @Test + public void parse_InsertSqlWithBigNumbersGiven_ShouldParseCorrectly() { + // Arrange + BigInteger bigInteger = BigInteger.valueOf(Integer.MAX_VALUE).add(BigInteger.ONE); + BigDecimal bigDecimal = new BigDecimal("1.234567890123456789"); + + // Act + List statements = + ScalarPartiqlParser.parse( + "INSERT INTO tbl VALUES { col1: " + + bigInteger + + " };" + + "INSERT INTO tbl VALUES { col1: " + + bigDecimal + + " };"); + + // Assert + assertThat(statements.size()).isEqualTo(2); + assertThat(statements.get(0)) + .isEqualTo( + InsertStatement.create("tbl", mapper.createObjectNode().put("col1", bigInteger))); + assertThat(statements.get(1)) + .isEqualTo( + InsertStatement.create("tbl", mapper.createObjectNode().put("col1", bigDecimal))); + } + + @Test + public void parse_InvalidInsertSqlGiven_ShouldThrowIllegalArgumentException() { + // Arrange + List sqlStatements = + ImmutableList.of( + "INSERT INTO tbl(col1, col2) VALUES ('aaa', 'bbb')", + "INSERT INTO tbl(col1, col2) VALUES {col1: 'aaa', col2: 'bbb'}", + "INSERT INTO tbl VALUES ['aaa', 'bbb']", + "INSERT INTO tbl VALUES {\"col1\": \"aaa\"}", + "INSERT INTO tbl VALUES {col1: 'aaa', col2: 'bbb'} ON CONFLICT(col1) DO NOTHING", + "INSERT INTO tbl AS t VALUES {col1: 'aaa', col2: 'bbb'}", + "INSERT INTO tbl DEFAULT VALUES", + "INSERT INTO tbl(col1, col2) SELECT col1, col2 FROM tbl2"); + + // Act Assert + for (String sql : sqlStatements) { + assertThatThrownBy(() -> ScalarPartiqlParser.parse(sql), sql) + .isInstanceOf(IllegalArgumentException.class); + } + } +} diff --git a/table-store/src/test/java/com/scalar/dl/tablestore/client/partiql/statement/CreateTableStatementTest.java b/table-store/src/test/java/com/scalar/dl/tablestore/client/partiql/statement/CreateTableStatementTest.java new file mode 100644 index 00000000..df5e4961 --- /dev/null +++ b/table-store/src/test/java/com/scalar/dl/tablestore/client/partiql/statement/CreateTableStatementTest.java @@ -0,0 +1,37 @@ +package com.scalar.dl.tablestore.client.partiql.statement; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.scalar.dl.tablestore.client.partiql.DataType; +import org.junit.jupiter.api.Test; + +public class CreateTableStatementTest { + + @Test + public void getArguments_CorrectStatementGiven_ShouldReturnCorrectArguments() { + // Arrange + CreateTableStatement statement = + CreateTableStatement.create( + "tbl", + "pkey", + DataType.STRING, + ImmutableMap.of( + "idx1", DataType.NUMBER, + "idx2", DataType.BOOLEAN)); + String expected = + "{\"name\":\"tbl\"," + + "\"key\":\"pkey\"," + + "\"type\":\"STRING\"," + + "\"indexes\":[" + + "{\"key\":\"idx1\",\"type\":\"NUMBER\"}," + + "{\"key\":\"idx2\",\"type\":\"BOOLEAN\"}" + + "]}"; + + // Act + String arguments = statement.getArguments(); + + // Assert + assertThat(arguments).isEqualTo(expected); + } +} diff --git a/table-store/src/test/java/com/scalar/dl/tablestore/client/partiql/statement/InsertStatementTest.java b/table-store/src/test/java/com/scalar/dl/tablestore/client/partiql/statement/InsertStatementTest.java new file mode 100644 index 00000000..64160b01 --- /dev/null +++ b/table-store/src/test/java/com/scalar/dl/tablestore/client/partiql/statement/InsertStatementTest.java @@ -0,0 +1,24 @@ +package com.scalar.dl.tablestore.client.partiql.statement; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +public class InsertStatementTest { + private static final ObjectMapper mapper = new ObjectMapper(); + + @Test + public void getArguments_CorrectStatementGiven_ShouldReturnCorrectArguments() { + // Arrange + InsertStatement statement = + InsertStatement.create("tbl", mapper.createObjectNode().put("col", "aaa")); + String expected = "{\"table\":\"tbl\",\"values\":{\"col\":\"aaa\"}}"; + + // Act + String arguments = statement.getArguments(); + + // Assert + assertThat(arguments).isEqualTo(expected); + } +}