From c8029f11e2ff638c266e797a86fa195dc74c469b Mon Sep 17 00:00:00 2001 From: dmitrybugakov Date: Fri, 22 Sep 2023 16:12:08 +0200 Subject: [PATCH] [#442]: Support LowCardinality series datatype --- .../housepower/data/DataTypeFactory.java | 18 +-- .../type/complex/DataTypeLowCardinality.java | 115 ++++++++++++++++++ .../jdbc/LowCardinalityTypeTest.java | 75 ++++++++++++ 3 files changed, 194 insertions(+), 14 deletions(-) create mode 100644 clickhouse-native-jdbc/src/main/java/com/github/housepower/data/type/complex/DataTypeLowCardinality.java create mode 100644 clickhouse-native-jdbc/src/test/java/com/github/housepower/jdbc/LowCardinalityTypeTest.java diff --git a/clickhouse-native-jdbc/src/main/java/com/github/housepower/data/DataTypeFactory.java b/clickhouse-native-jdbc/src/main/java/com/github/housepower/data/DataTypeFactory.java index bc9cfcfb..4f3d6f1d 100644 --- a/clickhouse-native-jdbc/src/main/java/com/github/housepower/data/DataTypeFactory.java +++ b/clickhouse-native-jdbc/src/main/java/com/github/housepower/data/DataTypeFactory.java @@ -34,19 +34,7 @@ import com.github.housepower.data.type.DataTypeUInt64; import com.github.housepower.data.type.DataTypeUInt8; import com.github.housepower.data.type.DataTypeUUID; -import com.github.housepower.data.type.complex.DataTypeArray; -import com.github.housepower.data.type.complex.DataTypeCreator; -import com.github.housepower.data.type.complex.DataTypeDateTime; -import com.github.housepower.data.type.complex.DataTypeDateTime64; -import com.github.housepower.data.type.complex.DataTypeDecimal; -import com.github.housepower.data.type.complex.DataTypeEnum16; -import com.github.housepower.data.type.complex.DataTypeEnum8; -import com.github.housepower.data.type.complex.DataTypeFixedString; -import com.github.housepower.data.type.complex.DataTypeMap; -import com.github.housepower.data.type.complex.DataTypeNothing; -import com.github.housepower.data.type.complex.DataTypeNullable; -import com.github.housepower.data.type.complex.DataTypeString; -import com.github.housepower.data.type.complex.DataTypeTuple; +import com.github.housepower.data.type.complex.*; import com.github.housepower.misc.LRUCache; import com.github.housepower.misc.SQLLexer; import com.github.housepower.misc.Validate; @@ -89,6 +77,8 @@ public class DataTypeFactory { return DataTypeDateTime64.creator.createDataType(lexer, serverContext); } else if (dataTypeName.equalsIgnoreCase("Nullable")) { return DataTypeNullable.creator.createDataType(lexer, serverContext); + } else if (dataTypeName.equalsIgnoreCase("LowCardinality")) { + return DataTypeLowCardinality.creator.createDataType(lexer, serverContext); } else if (dataTypeName.equalsIgnoreCase("FixedString") || dataTypeName.equals("Binary")) { return DataTypeFixedString.creator.createDataType(lexer, serverContext); } else if (dataTypeName.equalsIgnoreCase("Decimal")) { @@ -98,7 +88,7 @@ public class DataTypeFactory { } else if (dataTypeName.equalsIgnoreCase("Nothing")) { return DataTypeNothing.CREATOR.createDataType(lexer, serverContext); } else if (dataTypeName.equalsIgnoreCase("Map")) { - return DataTypeMap.creator.createDataType(lexer, serverContext); + return DataTypeMap.creator.createDataType(lexer, serverContext); } else { IDataType dataType = dataTypes.get(dataTypeName.toLowerCase(Locale.ROOT)); Validate.isTrue(dataType != null, "Unknown data type: " + dataTypeName); diff --git a/clickhouse-native-jdbc/src/main/java/com/github/housepower/data/type/complex/DataTypeLowCardinality.java b/clickhouse-native-jdbc/src/main/java/com/github/housepower/data/type/complex/DataTypeLowCardinality.java new file mode 100644 index 00000000..f40bd50a --- /dev/null +++ b/clickhouse-native-jdbc/src/main/java/com/github/housepower/data/type/complex/DataTypeLowCardinality.java @@ -0,0 +1,115 @@ +/* + * 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.github.housepower.data.type.complex; + +import com.github.housepower.data.DataTypeFactory; +import com.github.housepower.data.IDataType; +import com.github.housepower.misc.SQLLexer; +import com.github.housepower.misc.Validate; +import com.github.housepower.serde.BinaryDeserializer; +import com.github.housepower.serde.BinarySerializer; + +import java.io.IOException; +import java.sql.SQLException; + +public class DataTypeLowCardinality implements IDataType { + + public static DataTypeCreator creator = (lexer, serverContext) -> { + Validate.isTrue(lexer.character() == '('); + IDataType nestedType = DataTypeFactory.get(lexer, serverContext); + Validate.isTrue(lexer.character() == ')'); + return new DataTypeLowCardinality( + "LowCardinality(" + nestedType.name() + ")", nestedType); + }; + + private final String name; + private final IDataType nestedDataType; + + public DataTypeLowCardinality(String name, IDataType nestedDataType) { + this.name = name; + this.nestedDataType = nestedDataType; + } + + @Override + public String name() { + return this.name; + } + + @Override + public int sqlTypeId() { + return this.nestedDataType.sqlTypeId(); + } + + @Override + public Object defaultValue() { + return this.nestedDataType.defaultValue(); + } + + @Override + public Class javaType() { + return this.nestedDataType.javaType(); + } + + @Override + public Class jdbcJavaType() { + return this.nestedDataType.jdbcJavaType(); + } + + @Override + public boolean nullable() { + return this.nestedDataType.nullable(); + } + + @Override + public int getPrecision() { + return this.nestedDataType.getPrecision(); + } + + @Override + public int getScale() { + return this.nestedDataType.getScale(); + } + + @Override + public Object deserializeText(SQLLexer lexer) throws SQLException { + return this.nestedDataType.deserializeText(lexer); + } + + @Override + public void serializeBinary(Object data, BinarySerializer serializer) throws SQLException, IOException { + this.nestedDataType.serializeBinary(data, serializer); + } + + @Override + public void serializeBinaryBulk(Object[] data, BinarySerializer serializer) throws SQLException, IOException { + this.nestedDataType.serializeBinaryBulk(data, serializer); + } + + @Override + public Object deserializeBinary(BinaryDeserializer deserializer) throws SQLException, IOException { + return this.nestedDataType.deserializeBinary(deserializer); + } + + @Override + public Object[] deserializeBinaryBulk(int rows, BinaryDeserializer deserializer) throws SQLException, IOException { + Object[] data = this.nestedDataType.deserializeBinaryBulk(rows, deserializer); + return data; + } + + @Override + public boolean isSigned() { + return this.nestedDataType.isSigned(); + } +} diff --git a/clickhouse-native-jdbc/src/test/java/com/github/housepower/jdbc/LowCardinalityTypeTest.java b/clickhouse-native-jdbc/src/test/java/com/github/housepower/jdbc/LowCardinalityTypeTest.java new file mode 100644 index 00000000..052e8820 --- /dev/null +++ b/clickhouse-native-jdbc/src/test/java/com/github/housepower/jdbc/LowCardinalityTypeTest.java @@ -0,0 +1,75 @@ +/* + * 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.github.housepower.jdbc; + +import com.github.housepower.misc.BytesHelper; +import org.junit.jupiter.api.Test; + +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +import static org.junit.jupiter.api.Assertions.*; + +//Refer to [[https://github.com/housepower/ClickHouse-Native-JDBC/issues/442]] for more details. +public class LowCardinalityTypeTest extends AbstractITest implements BytesHelper { + @Test + public void testLowCardinalityType() throws Exception { + withStatement(statement -> { + statement.execute("DROP TABLE IF EXISTS low_cardinality_test"); + statement.execute("CREATE TABLE IF NOT EXISTS low_cardinality_test " + + "(value LowCardinality(String), nullable_value LowCardinality(Nullable(String))) Engine=Memory()"); + + String testValue = "value"; + String sql = "INSERT INTO low_cardinality_test (value, nullable_value) values(?, ?);"; + try (PreparedStatement pstmt = statement.getConnection().prepareStatement(sql)) { + for (int i = 0; i < 300; i++) { + pstmt.setString(1, testValue); + pstmt.setString(2, null); + pstmt.addBatch(); + } + pstmt.executeBatch(); + } + + DatabaseMetaData metaData = statement.getConnection().getMetaData(); + ResultSet columns = metaData.getColumns(null, "default", "low_cardinality_test", "%value%"); + while (columns.next()) { + String columnName = columns.getString("COLUMN_NAME"); + String columnType = columns.getString("TYPE_NAME"); + boolean nullable = columns.getInt("NULLABLE") == DatabaseMetaData.columnNullable; + + if (columnName.equals("value")) { + assertEquals(columnType, "LowCardinality(String)"); + assertFalse(nullable); + } else { + assertEquals(columnType, "LowCardinality(Nullable(String))"); + assertTrue(nullable); + } + } + + ResultSet rs = statement.executeQuery("SELECT * FROM low_cardinality_test;"); + int size = 0; + while (rs.next()) { + String value = rs.getString(1); + String nullableValue = rs.getString(2); + assertEquals(testValue, value); + assertNull(nullableValue); + size++; + } + assertEquals(300, size); + statement.execute("DROP TABLE IF EXISTS decimal_test"); + }); + } +}