This is an automated email from the ASF dual-hosted git repository.
jackietien pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new cb4281cac55 Fix insert case sensitivity of table model
cb4281cac55 is described below
commit cb4281cac5551ba60fb7d45ac803152b2adbd0f8
Author: Jiang Tian <[email protected]>
AuthorDate: Thu Aug 15 15:32:23 2024 +0800
Fix insert case sensitivity of table model
---
.../relational/it/db/it/IoTDBInsertTableIT.java | 36 ++++++++++++++++++++++
.../plan/analyze/schema/SchemaValidator.java | 1 +
.../plan/relational/sql/ast/InsertRow.java | 10 +-----
.../plan/relational/sql/ast/InsertRows.java | 20 ++++++------
.../plan/relational/sql/ast/InsertTablet.java | 9 +-----
.../relational/sql/ast/WrappedInsertStatement.java | 4 +++
.../plan/relational/sql/parser/AstBuilder.java | 12 +++++---
.../plan/statement/crud/InsertBaseStatement.java | 24 +++++++++++++++
.../crud/InsertMultiTabletsStatement.java | 28 +++++++++++++++++
.../crud/InsertRowsOfOneDeviceStatement.java | 28 +++++++++++++++++
.../plan/statement/crud/InsertRowsStatement.java | 28 +++++++++++++++++
11 files changed, 167 insertions(+), 33 deletions(-)
diff --git
a/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBInsertTableIT.java
b/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBInsertTableIT.java
index 76be6647d4c..1908b36f80f 100644
---
a/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBInsertTableIT.java
+++
b/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/IoTDBInsertTableIT.java
@@ -557,6 +557,7 @@ public class IoTDBInsertTableIT {
st1.execute("flush");
st1.execute("insert into sg17(id1, time, s1) values('d1', 604799990,1),
('d1', 604800001,1)");
st1.execute("flush");
+
ResultSet rs1 = st1.executeQuery("select time, s1 from sg17");
assertTrue(rs1.next());
assertEquals(604799990, rs1.getLong("time"));
@@ -577,6 +578,41 @@ public class IoTDBInsertTableIT {
"create table if not exists sg18 (id1 string id, s1 string
attribute, s2 int32 measurement)");
st1.execute("insert into sg18(id1, s1, s2) values('d1','1', 1)");
st1.execute("insert into sg18(id1, s1, s2) values('d2', 2, 2)");
+
+ ResultSet rs1 = st1.executeQuery("select time, s1, s2 from sg18 order by
s1");
+ assertTrue(rs1.next());
+ assertEquals("1", rs1.getString("s1"));
+ assertTrue(rs1.next());
+ assertEquals("2", rs1.getString("s1"));
+ assertFalse(rs1.next());
+ }
+ }
+
+ @Test
+ public void testInsertCaseSensitivity() throws SQLException {
+ try (Connection connection =
EnvFactory.getEnv().getConnection(BaseEnv.TABLE_SQL_DIALECT);
+ Statement st1 = connection.createStatement()) {
+ st1.execute("use \"test\"");
+ st1.execute(
+ "create table if not exists sg19 (id1 string id, ss1 string
attribute, ss2 int32 measurement)");
+ // lower case
+ st1.execute("insert into sg19(time, id1, ss1, ss2) values(1, 'd1','1',
1)");
+ st1.execute("insert into sg19(time, id1, ss1, ss2) values(2, 'd2', 2,
2)");
+ // upper case
+ st1.execute("insert into sg19(TIME, ID1, SS1, SS2) values(3, 'd3','3',
3)");
+ st1.execute("insert into sg19(TIME, ID1, SS1, SS2) values(4, 'd4', 4,
4)");
+ // mixed
+ st1.execute("insert into sg19(TIme, Id1, Ss1, Ss2) values(5, 'd5','5',
5)");
+ st1.execute("insert into sg19(TIme, Id1, sS1, sS2) values(6, 'd6', 6,
6)");
+
+ ResultSet rs1 = st1.executeQuery("select time, ss1, ss2 from sg19 order
by time");
+ for (int i = 1; i <= 6; i++) {
+ assertTrue(rs1.next());
+ assertEquals(i, rs1.getLong("time"));
+ assertEquals(String.valueOf(i), rs1.getString("ss1"));
+ assertEquals(i, rs1.getInt("ss2"));
+ }
+ assertFalse(rs1.next());
}
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/schema/SchemaValidator.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/schema/SchemaValidator.java
index bc4d275c285..2458ff36338 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/schema/SchemaValidator.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/schema/SchemaValidator.java
@@ -64,6 +64,7 @@ public class SchemaValidator {
public static void validate(
Metadata metadata, WrappedInsertStatement insertStatement,
MPPQueryContext context) {
try {
+ insertStatement.columnsToLowerCase();
insertStatement.validateTableSchema(metadata, context);
insertStatement.updateAfterSchemaValidation(context);
insertStatement.validateDeviceSchema(metadata, context);
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertRow.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertRow.java
index e345cb59b60..cbce78e6528 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertRow.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertRow.java
@@ -19,12 +19,10 @@
package org.apache.iotdb.db.queryengine.plan.relational.sql.ast;
-import org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory;
import org.apache.iotdb.db.exception.query.QueryProcessException;
import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -65,13 +63,7 @@ public class InsertRow extends WrappedInsertStatement {
@Override
public List<String> getAttributeColumnNameList() {
final InsertRowStatement insertRowStatement = getInnerTreeStatement();
- List<String> result = new ArrayList<>();
- for (int i = 0; i < insertRowStatement.getColumnCategories().length; i++) {
- if (insertRowStatement.getColumnCategories()[i] ==
TsTableColumnCategory.ATTRIBUTE) {
- result.add(insertRowStatement.getMeasurements()[i]);
- }
- }
- return result;
+ return insertRowStatement.getAttributeColumnNameList();
}
@Override
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertRows.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertRows.java
index 361a6ccb540..e47912e6bc1 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertRows.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertRows.java
@@ -23,6 +23,7 @@ import
org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory;
import org.apache.iotdb.db.exception.query.QueryProcessException;
import org.apache.iotdb.db.exception.sql.SemanticException;
import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
+import org.apache.iotdb.db.queryengine.plan.analyze.AnalyzeUtils;
import
org.apache.iotdb.db.queryengine.plan.relational.metadata.ITableDeviceSchemaValidation;
import org.apache.iotdb.db.queryengine.plan.relational.metadata.Metadata;
import org.apache.iotdb.db.queryengine.plan.relational.metadata.TableSchema;
@@ -80,13 +81,16 @@ public class InsertRows extends WrappedInsertStatement {
@Override
public void validateTableSchema(Metadata metadata, MPPQueryContext context) {
- String databaseName = getDatabase();
for (InsertRowStatement insertRowStatement :
getInnerTreeStatement().getInsertRowStatementList()) {
final TableSchema incomingTableSchema =
toTableSchema(insertRowStatement);
final TableSchema realSchema =
metadata
- .validateTableHeaderSchema(databaseName, incomingTableSchema,
context, false)
+ .validateTableHeaderSchema(
+ AnalyzeUtils.getDatabaseName(insertRowStatement, context),
+ incomingTableSchema,
+ context,
+ false)
.orElse(null);
if (realSchema == null) {
throw new SemanticException(
@@ -110,12 +114,12 @@ public class InsertRows extends WrappedInsertStatement {
@Override
public String getDatabase() {
- return InsertRows.this.getDatabase();
+ return AnalyzeUtils.getDatabaseName(insertRowStatement, context);
}
@Override
public String getTableName() {
- return InsertRows.this.getTableName();
+ return insertRowStatement.getTableDeviceID().getTableName();
}
@Override
@@ -126,13 +130,7 @@ public class InsertRows extends WrappedInsertStatement {
@Override
public List<String> getAttributeColumnNameList() {
- List<String> attributeColumnNameList = new ArrayList<>();
- for (int i = 0; i < insertRowStatement.getColumnCategories().length;
i++) {
- if (insertRowStatement.getColumnCategories()[i] ==
TsTableColumnCategory.ATTRIBUTE) {
-
attributeColumnNameList.add(insertRowStatement.getMeasurements()[i]);
- }
- }
- return attributeColumnNameList;
+ return insertRowStatement.getAttributeColumnNameList();
}
@Override
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertTablet.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertTablet.java
index b036d98a6ab..4feecf2807c 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertTablet.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/InsertTablet.java
@@ -19,7 +19,6 @@
package org.apache.iotdb.db.queryengine.plan.relational.sql.ast;
-import org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory;
import org.apache.iotdb.db.exception.query.QueryProcessException;
import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
import
org.apache.iotdb.db.queryengine.plan.statement.crud.InsertTabletStatement;
@@ -71,13 +70,7 @@ public class InsertTablet extends WrappedInsertStatement {
@Override
public List<String> getAttributeColumnNameList() {
final InsertTabletStatement insertTabletStatement =
getInnerTreeStatement();
- List<String> result = new ArrayList<>();
- for (int i = 0; i < insertTabletStatement.getColumnCategories().length;
i++) {
- if (insertTabletStatement.getColumnCategories()[i] ==
TsTableColumnCategory.ATTRIBUTE) {
- result.add(insertTabletStatement.getMeasurements()[i]);
- }
- }
- return result;
+ return insertTabletStatement.getAttributeColumnNameList();
}
@Override
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/WrappedInsertStatement.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/WrappedInsertStatement.java
index e8481557784..f86ed3ab9f8 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/WrappedInsertStatement.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/ast/WrappedInsertStatement.java
@@ -218,4 +218,8 @@ public abstract class WrappedInsertStatement extends
WrappedStatement
}
return databaseName;
}
+
+ public void columnsToLowerCase() {
+ getInnerTreeStatement().measurementsToLowerCase();
+ }
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java
index b2e98656fa1..391d5cd0ff2 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/sql/parser/AstBuilder.java
@@ -411,20 +411,21 @@ public class AstBuilder extends
RelationalSqlBaseVisitor<Node> {
.map(
r ->
toInsertRowStatement(
- ((Row) r), finalTimeColumnIndex, columnNameArray,
tableName))
+ ((Row) r), finalTimeColumnIndex, columnNameArray,
tableName, databaseName))
.collect(toList());
InsertRowsStatement insertRowsStatement = new InsertRowsStatement();
insertRowsStatement.setInsertRowStatementList(rowStatements);
insertRowsStatement.setWriteToTable(true);
- insertRowsStatement.setDevicePath(new PartialPath(new String[]
{tableName}));
- databaseName.ifPresent(insertRowsStatement::setDatabaseName);
- insertRowsStatement.setMeasurements(columnNameArray);
return new InsertRows(insertRowsStatement, null);
}
private InsertRowStatement toInsertRowStatement(
- Row row, int timeColumnIndex, String[] nonTimeColumnNames, String
tableName) {
+ Row row,
+ int timeColumnIndex,
+ String[] nonTimeColumnNames,
+ String tableName,
+ Optional<String> databaseName) {
InsertRowStatement insertRowStatement = new InsertRowStatement();
insertRowStatement.setWriteToTable(true);
insertRowStatement.setDevicePath(new PartialPath(new String[]
{tableName}));
@@ -468,6 +469,7 @@ public class AstBuilder extends
RelationalSqlBaseVisitor<Node> {
insertRowStatement.setValues(values);
insertRowStatement.setNeedInferType(true);
+ databaseName.ifPresent(insertRowStatement::setDatabaseName);
return insertRowStatement;
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertBaseStatement.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertBaseStatement.java
index 8bccc30f493..c852651802c 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertBaseStatement.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertBaseStatement.java
@@ -551,5 +551,29 @@ public abstract class InsertBaseStatement extends
Statement {
public Optional<String> getDatabaseName() {
return Optional.ofNullable(databaseName);
}
+
// endregion
+
+ @TableModel
+ public void measurementsToLowerCase() {
+ if (measurements == null) {
+ return;
+ }
+ for (int i = 0; i < measurements.length; i++) {
+ if (measurements[i] != null) {
+ measurements[i] = measurements[i].toLowerCase();
+ }
+ }
+ }
+
+ @TableModel
+ public List<String> getAttributeColumnNameList() {
+ List<String> attributeColumnNameList = new ArrayList<>();
+ for (int i = 0; i < getColumnCategories().length; i++) {
+ if (getColumnCategories()[i] == TsTableColumnCategory.ATTRIBUTE) {
+ attributeColumnNameList.add(getMeasurements()[i]);
+ }
+ }
+ return attributeColumnNameList;
+ }
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertMultiTabletsStatement.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertMultiTabletsStatement.java
index bfc4e6406c6..b92cf23ba19 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertMultiTabletsStatement.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertMultiTabletsStatement.java
@@ -20,16 +20,20 @@
package org.apache.iotdb.db.queryengine.plan.statement.crud;
import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.db.exception.sql.SemanticException;
import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
import org.apache.iotdb.db.queryengine.plan.analyze.schema.ISchemaValidation;
import org.apache.iotdb.db.queryengine.plan.statement.StatementType;
import org.apache.iotdb.db.queryengine.plan.statement.StatementVisitor;
+import org.apache.iotdb.db.utils.annotations.TableModel;
import org.apache.tsfile.enums.TSDataType;
import org.apache.tsfile.exception.NotImplementedException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
import java.util.stream.Collectors;
public class InsertMultiTabletsStatement extends InsertBaseStatement {
@@ -144,4 +148,28 @@ public class InsertMultiTabletsStatement extends
InsertBaseStatement {
splitResult.setInsertTabletStatementList(mergedList);
return splitResult;
}
+
+ @TableModel
+ @Override
+ public void measurementsToLowerCase() {
+
insertTabletStatementList.forEach(InsertTabletStatement::measurementsToLowerCase);
+ }
+
+ @Override
+ @TableModel
+ public Optional<String> getDatabaseName() {
+ Optional<String> database = Optional.empty();
+ for (InsertTabletStatement insertTabletStatement :
insertTabletStatementList) {
+ Optional<String> childDatabaseName =
insertTabletStatement.getDatabaseName();
+ if (childDatabaseName.isPresent()
+ && database.isPresent()
+ && !Objects.equals(childDatabaseName.get(), database.get())) {
+ throw new SemanticException(
+ "Cannot insert into multiple databases within one statement,
please split them manually");
+ }
+
+ database = childDatabaseName;
+ }
+ return database;
+ }
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertRowsOfOneDeviceStatement.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertRowsOfOneDeviceStatement.java
index 66bdcd35feb..b98ee464067 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertRowsOfOneDeviceStatement.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertRowsOfOneDeviceStatement.java
@@ -23,10 +23,12 @@ import
org.apache.iotdb.common.rpc.thrift.TTimePartitionSlot;
import org.apache.iotdb.commons.path.PartialPath;
import org.apache.iotdb.commons.utils.TimePartitionUtils;
import org.apache.iotdb.db.exception.query.QueryProcessException;
+import org.apache.iotdb.db.exception.sql.SemanticException;
import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
import org.apache.iotdb.db.queryengine.plan.analyze.schema.ISchemaValidation;
import org.apache.iotdb.db.queryengine.plan.statement.StatementType;
import org.apache.iotdb.db.queryengine.plan.statement.StatementVisitor;
+import org.apache.iotdb.db.utils.annotations.TableModel;
import org.apache.tsfile.enums.TSDataType;
import org.apache.tsfile.exception.NotImplementedException;
@@ -34,6 +36,8 @@ import org.apache.tsfile.exception.NotImplementedException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@@ -167,4 +171,28 @@ public class InsertRowsOfOneDeviceStatement extends
InsertBaseStatement {
}
return this;
}
+
+ @TableModel
+ @Override
+ public void measurementsToLowerCase() {
+
insertRowStatementList.forEach(InsertRowStatement::measurementsToLowerCase);
+ }
+
+ @Override
+ @TableModel
+ public Optional<String> getDatabaseName() {
+ Optional<String> database = Optional.empty();
+ for (InsertRowStatement rowStatement : insertRowStatementList) {
+ Optional<String> childDatabaseName = rowStatement.getDatabaseName();
+ if (childDatabaseName.isPresent()
+ && database.isPresent()
+ && !Objects.equals(childDatabaseName.get(), database.get())) {
+ throw new SemanticException(
+ "Cannot insert into multiple databases within one statement,
please split them manually");
+ }
+
+ database = childDatabaseName;
+ }
+ return database;
+ }
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertRowsStatement.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertRowsStatement.java
index 68a0264a508..94553269cf0 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertRowsStatement.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/statement/crud/InsertRowsStatement.java
@@ -21,10 +21,12 @@ package org.apache.iotdb.db.queryengine.plan.statement.crud;
import org.apache.iotdb.commons.path.PartialPath;
import org.apache.iotdb.db.exception.query.QueryProcessException;
+import org.apache.iotdb.db.exception.sql.SemanticException;
import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
import org.apache.iotdb.db.queryengine.plan.analyze.schema.ISchemaValidation;
import org.apache.iotdb.db.queryengine.plan.statement.StatementType;
import org.apache.iotdb.db.queryengine.plan.statement.StatementVisitor;
+import org.apache.iotdb.db.utils.annotations.TableModel;
import org.apache.tsfile.enums.TSDataType;
import org.apache.tsfile.exception.NotImplementedException;
@@ -32,6 +34,8 @@ import org.apache.tsfile.exception.NotImplementedException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
import java.util.stream.Collectors;
public class InsertRowsStatement extends InsertBaseStatement {
@@ -173,4 +177,28 @@ public class InsertRowsStatement extends
InsertBaseStatement {
})
.collect(Collectors.toList());
}
+
+ @TableModel
+ @Override
+ public void measurementsToLowerCase() {
+
insertRowStatementList.forEach(InsertRowStatement::measurementsToLowerCase);
+ }
+
+ @Override
+ @TableModel
+ public Optional<String> getDatabaseName() {
+ Optional<String> database = Optional.empty();
+ for (InsertRowStatement rowStatement : insertRowStatementList) {
+ Optional<String> childDatabaseName = rowStatement.getDatabaseName();
+ if (childDatabaseName.isPresent()
+ && database.isPresent()
+ && !Objects.equals(childDatabaseName.get(), database.get())) {
+ throw new SemanticException(
+ "Cannot insert into multiple databases within one statement,
please split them manually");
+ }
+
+ database = childDatabaseName;
+ }
+ return database;
+ }
}