diff --git a/dialect/db/hive/pom.xml b/dialect/db/hive/pom.xml new file mode 100644 index 0000000..f1cd519 --- /dev/null +++ b/dialect/db/hive/pom.xml @@ -0,0 +1,92 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db + ${revision} + ../pom.xml + + org.eclipse.daanse.sql.dialect.db.hive + Eclipse Daanse JDBC DB Dialect Hive + + + 4.2.0 + + + + + org.slf4j + slf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common + ${revision} + + + biz.aQute.bnd + biz.aQute.bndlib + + + + org.apache.hive + hive-jdbc + ${hive-jdbc.version} + standalone + test + + + * + * + + + + + org.testcontainers + junit-jupiter + 1.19.7 + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.record + ${project.version} + test + + + + + + maven-surefire-plugin + + + + ${env.DOCKER_HOST} + + + + + + diff --git a/dialect/db/hive/src/main/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialect.java b/dialect/db/hive/src/main/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialect.java new file mode 100644 index 0000000..b2b940a --- /dev/null +++ b/dialect/db/hive/src/main/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialect.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.hive; + +import java.sql.Date; +import java.sql.Timestamp; +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding; +import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; +import org.eclipse.daanse.sql.dialect.db.common.DialectUtil; + +/** + * Dialect for the Apache Hive database (HiveQL over HiveServer2). + */ +public class HiveDialect extends AbstractJdbcDialect { + + private static final String SUPPORTED_PRODUCT_NAME = "HIVE"; + + /** JDBC-free constructor for SQL generation. Uses HiveQL backtick quoting. */ + public HiveDialect() { + super(DialectInitData.ansiDefaults().withQuoteIdentifierString("`")); + } + + /** Construct from a captured snapshot — the canonical entry point. */ + public HiveDialect(DialectInitData init) { + super(init); + } + + @Override + public boolean supportsResultSetConcurrency(int type, int concurrency) { + return false; + } + + @Override + public boolean allowsCompoundCountDistinct() { + return true; + } + + @Override + public boolean requiresAliasForFromQuery() { + return true; + } + + @Override + public boolean requiresOrderByAlias() { + return true; + } + + // Explicit even though it matches the derived default here: a subclass that + // flips requiresOrderByAlias() to false must keep ORDER BY aliases allowed. + @Override + public boolean allowsOrderByAlias() { + return true; + } + + @Override + public boolean requiresUnionOrderByExprInSelect() { + return false; + } + + @Override + public boolean requiresUnionOrderByOrdinal() { + return false; + } + + @Override + public boolean allowsJoinOn() { + return false; + } + + @Override + public StringBuilder generateInline(List columnNames, List columnTypes, List valueList) { + // The legacy (Mondrian-era) form appended " from dual", which fails on + // any modern Hive ("Table not found 'dual'" — verified against Hive 4); + // Hive supports FROM-less SELECTs since 0.13. + return new StringBuilder("select * from (") + .append(generateInlineGeneric(columnNames, columnTypes, valueList, null, false)) + .append(") x limit ").append(valueList.size()); + } + + @Override + protected void quoteDateLiteral(StringBuilder buf, Date date) { + // Hive doesn't support Date type; treat date as a string '2008-01-23' + DialectUtil.singleQuoteString(date.toString(), buf); + } + + @Override + protected void quoteTimestampLiteral(StringBuilder buf, String value, Timestamp timestamp) { + buf.append("cast( "); + DialectUtil.singleQuoteString(value, buf); + buf.append(" as timestamp )"); + } + + @Override + public StringBuilder generateOrderByNulls(CharSequence expr, boolean ascending, boolean collateNullsLast) { + // In Hive, Null values are worth negative infinity. + return DialectUtil.generateOrderByNullsWithIsnull(expr, ascending, collateNullsLast); + } + + @Override + public IdentifierCaseFolding caseFolding() { + return IdentifierCaseFolding.PRESERVE; + } + + @Override + public String name() { + return SUPPORTED_PRODUCT_NAME.toLowerCase(); + } + +} diff --git a/dialect/db/hive/src/main/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialectFactory.java b/dialect/db/hive/src/main/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialectFactory.java new file mode 100644 index 0000000..2ee6dec --- /dev/null +++ b/dialect/db/hive/src/main/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialectFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.hive; + +import java.util.function.Function; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.DialectName; +import org.eclipse.daanse.sql.dialect.db.common.AbstractDialectFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +@Component(service = DialectFactory.class, scope = ServiceScope.SINGLETON) +@DialectName("HIVE") +public class HiveDialectFactory extends AbstractDialectFactory { + + @Override + public Function getConstructorFunction() { + return HiveDialect::new; + } + +} diff --git a/dialect/db/hive/src/main/java/org/eclipse/daanse/sql/dialect/db/hive/package-info.java b/dialect/db/hive/src/main/java/org/eclipse/daanse/sql/dialect/db/hive/package-info.java new file mode 100644 index 0000000..0c94ecb --- /dev/null +++ b/dialect/db/hive/src/main/java/org/eclipse/daanse/sql/dialect/db/hive/package-info.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.db.hive; diff --git a/dialect/db/hive/src/test/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialectQuotingPolicyTest.java b/dialect/db/hive/src/test/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialectQuotingPolicyTest.java new file mode 100644 index 0000000..ec28df6 --- /dev/null +++ b/dialect/db/hive/src/test/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialectQuotingPolicyTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.hive; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.junit.jupiter.api.Test; + +class HiveDialectQuotingPolicyTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "TEST"); + private static final TableReference EMP = new TableReference(Optional.of(S), "EMPLOYEES", + TableReference.TYPE_TABLE); + private static final TableReference DEPT = new TableReference(Optional.of(S), "DEPARTMENTS", + TableReference.TYPE_TABLE); + + private HiveDialect dialectNever() { + HiveDialect d = new HiveDialect(); + d.setQuotingPolicy(IdentifierQuotingPolicy.NEVER); + return d; + } + + @Test + void primaryKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addPrimaryKeyConstraint(EMP, "PK_EMP", List.of("ID"))) + .doesNotContain("`").contains("EMPLOYEES").contains("PK_EMP").contains("ID"); + } + + @Test + void uniqueConstraint_unquoted() { + assertThat(dialectNever().ddlGenerator().addUniqueConstraint(EMP, "UQ_EMP", List.of("EMAIL"))) + .doesNotContain("`").contains("EMPLOYEES").contains("UQ_EMP").contains("EMAIL"); + } + + @Test + void foreignKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addForeignKeyConstraint(EMP, "FK_EMP_DEPT", List.of("DEPT_ID"), DEPT, + List.of("ID"), "NO ACTION", "NO ACTION")).doesNotContain("`").contains("EMPLOYEES") + .contains("FK_EMP_DEPT").contains("DEPARTMENTS"); + } + + @Test + void renameTable_unquoted() { + assertThat(dialectNever().ddlGenerator().renameTable(EMP, "PERSON")).doesNotContain("`").contains("EMPLOYEES") + .contains("PERSON"); + } + + @Test + void createIndex_unquoted() { + assertThat(dialectNever().ddlGenerator().createIndex("IDX_EMP", EMP, List.of("NAME"), false, false)) + .doesNotContain("`").contains("EMPLOYEES").contains("IDX_EMP").contains("NAME"); + } + + @Test + void dropConstraint_unquoted() { + assertThat(dialectNever().ddlGenerator().dropConstraint(EMP, "PK_EMP", true)).doesNotContain("`") + .contains("EMPLOYEES").contains("PK_EMP"); + } +} diff --git a/dialect/db/hive/src/test/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialectTest.java b/dialect/db/hive/src/test/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialectTest.java new file mode 100644 index 0000000..1b128da --- /dev/null +++ b/dialect/db/hive/src/test/java/org/eclipse/daanse/sql/dialect/db/hive/HiveDialectTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.hive; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.ResultSet; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class HiveDialectTest { + + private HiveDialect dialect; + + @BeforeEach + void setUp() { + dialect = new HiveDialect(); + } + + @Test + void testName() { + assertEquals("hive", dialect.name()); + } + + @Test + void testQuoteIdentifierString() { + assertEquals("`", dialect.getQuoteIdentifierString()); + } + + @Test + void testSupportsResultSetConcurrency() { + assertFalse(dialect.supportsResultSetConcurrency(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)); + } + + @Test + void testRequiresAliasForFromQuery() { + assertTrue(dialect.requiresAliasForFromQuery()); + } + + @Test + void testRequiresOrderByAlias() { + assertTrue(dialect.requiresOrderByAlias()); + } + + @Test + void testAllowsOrderByAlias() { + assertTrue(dialect.allowsOrderByAlias()); + } + + @Test + void testRequiresGroupByAlias() { + assertFalse(dialect.requiresGroupByAlias()); + } + + @Test + void testAllowsCompoundCountDistinct() { + assertTrue(dialect.allowsCompoundCountDistinct()); + } + + @Test + void testAllowsJoinOn() { + assertFalse(dialect.allowsJoinOn()); + } + + @Test + void testRequiresUnionOrderByExprInSelect() { + assertFalse(dialect.requiresUnionOrderByExprInSelect()); + } + + @Test + void testRequiresUnionOrderByOrdinal() { + assertFalse(dialect.requiresUnionOrderByOrdinal()); + } + + @Test + void testQuoteDateLiteral() { + StringBuilder buf = new StringBuilder(); + dialect.quoteDateLiteral(buf, java.sql.Date.valueOf("2024-01-15")); + assertEquals("'2024-01-15'", buf.toString()); + } + + @Test + void testQuoteTimestampLiteral() { + StringBuilder buf = new StringBuilder(); + // The public overload normalizes through Timestamp.toString(), hence the ".0". + dialect.quoteTimestampLiteral(buf, "2024-01-15 10:30:00"); + assertEquals("cast( '2024-01-15 10:30:00.0' as timestamp )", buf.toString()); + } + + @Test + void testQuoteTimestampLiteral_InvalidFormat() { + StringBuilder buf = new StringBuilder(); + assertThrows(NumberFormatException.class, () -> dialect.quoteTimestampLiteral(buf, "invalid")); + } + + @Test + void testGenerateOrderByNulls_AscNullsLast() { + assertEquals("ISNULL(column1) ASC, column1 ASC", dialect.generateOrderByNulls("column1", true, true).toString()); + } + + @Test + void testGenerateOrderByNulls_AscNullsFirst() { + assertEquals("column1 ASC", dialect.generateOrderByNulls("column1", true, false).toString()); + } + + @Test + void testGenerateOrderByNulls_DescNullsLast() { + assertEquals("column1 DESC", dialect.generateOrderByNulls("column1", false, true).toString()); + } + + @Test + void testGenerateOrderByNulls_DescNullsFirst() { + assertEquals("ISNULL(column1) DESC, column1 DESC", dialect.generateOrderByNulls("column1", false, false).toString()); + } + + @Test + void testGenerateInline() { + String sql = dialect + .generateInline(java.util.List.of("c1"), java.util.List.of("String"), + java.util.List.of(new String[] { "a" })) + .toString(); + assertTrue(sql.startsWith("select * from (")); + assertTrue(sql.endsWith(") x limit 1")); + assertFalse(sql.contains("dual")); // no built-in dual table in Hive — verified against Hive 4 + } +} diff --git a/dialect/db/hive/src/test/java/org/eclipse/daanse/sql/dialect/db/hive/integration/ServiceTest.java b/dialect/db/hive/src/test/java/org/eclipse/daanse/sql/dialect/db/hive/integration/ServiceTest.java new file mode 100644 index 0000000..608933f --- /dev/null +++ b/dialect/db/hive/src/test/java/org/eclipse/daanse/sql/dialect/db/hive/integration/ServiceTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.hive.integration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.db.hive.HiveDialectFactory; +import org.junit.jupiter.api.Test; +import org.osgi.test.common.annotation.InjectService; + +class ServiceTest { + @Test + void serviceExists(@InjectService List dialects) throws Exception { + assertThat(dialects).isNotNull().isNotEmpty().anyMatch(HiveDialectFactory.class::isInstance); + } +} diff --git a/dialect/db/hive/src/test/java/org/eclipse/daanse/sql/dialect/db/hive/sqlgen/HiveDdlRoundTripTest.java b/dialect/db/hive/src/test/java/org/eclipse/daanse/sql/dialect/db/hive/sqlgen/HiveDdlRoundTripTest.java new file mode 100644 index 0000000..68836d8 --- /dev/null +++ b/dialect/db/hive/src/test/java/org/eclipse/daanse/sql/dialect/db/hive/sqlgen/HiveDdlRoundTripTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.hive.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.JDBCType; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Properties; + +import org.apache.hive.jdbc.HiveDriver; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.db.hive.HiveDialect; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnDefinitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +/** + * Round trip against a real HiveServer2 (apache/hive quickstart image, + * standalone local execution). Hive has no enforced unique/foreign keys and no + * CREATE INDEX (removed in Hive 3), so the trip covers what the dialect + * actually emits for Hive: schema + table DDL, backtick quoting, the + * ISNULL-based ORDER BY NULLS emulation, the {@code cast(... as timestamp)} + * literal form, a view, and the drops. + */ +@Testcontainers +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +@TestInstance(Lifecycle.PER_CLASS) +class HiveDdlRoundTripTest { + + private static final int HS2_PORT = 10000; + + @Container + @SuppressWarnings("resource") + static final GenericContainer CONTAINER = new GenericContainer<>("apache/hive:4.2.0") + .withEnv("SERVICE_NAME", "hiveserver2").withExposedPorts(HS2_PORT) + .waitingFor(Wait.forListeningPort()).withStartupTimeout(Duration.ofMinutes(5)); + + private static final SchemaReference SCHEMA = new SchemaReference(Optional.empty(), "RT_TEST"); + private static final TableReference CUSTOMERS = new TableReference(Optional.of(SCHEMA), "CUSTOMERS", + TableReference.TYPE_TABLE); + private static final TableReference VIEW = new TableReference(Optional.of(SCHEMA), "CUSTOMER_NAMES", + TableReference.TYPE_VIEW); + + private Dialect dialect; + private Connection connection; + + @BeforeAll + void setUp() throws Exception { + this.dialect = new HiveDialect(); + String url = "jdbc:hive2://" + CONTAINER.getHost() + ":" + CONTAINER.getMappedPort(HS2_PORT) + "/default"; + // HS2 accepts TCP before sessions work — retry until a statement runs. + HiveDriver driver = new HiveDriver(); + SQLException last = null; + for (int i = 0; i < 48; i++) { + try { + Connection c = driver.connect(url, new Properties()); + try (Statement s = c.createStatement()) { + s.execute("SELECT 1"); + } + this.connection = c; + return; + } catch (SQLException e) { + last = e; + Thread.sleep(5000); + } + } + throw new IllegalStateException("HiveServer2 did not become ready", last); + } + + @AfterAll + void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) + connection.close(); + } + + private static ColumnDefinition col(TableReference table, String name, JDBCType jdbc, + ColumnMetaData.Nullability nullability, OptionalInt size, OptionalInt scale) { + ColumnReference ref = new ColumnReference(Optional.of(table), name); + ColumnMetaData meta = new ColumnMetaDataRecord(jdbc, jdbc.getName(), size, scale, OptionalInt.empty(), + nullability, OptionalInt.empty(), Optional.empty(), Optional.empty(), + ColumnMetaData.AutoIncrement.UNKNOWN, ColumnMetaData.GeneratedColumn.UNKNOWN); + return new ColumnDefinitionRecord(ref, meta); + } + + private void execute(String sql) throws SQLException { + try (Statement s = connection.createStatement()) { + s.execute(sql); + } + } + + @Test + void full_round_trip() throws Exception { + List custCols = List.of( + col(CUSTOMERS, "ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(CUSTOMERS, "EMAIL", JDBCType.VARCHAR, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.of(100), + OptionalInt.empty()), + col(CUSTOMERS, "NAME", JDBCType.VARCHAR, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(50), + OptionalInt.empty())); + + // CREATE — no PK: Hive constraints are informational only. + execute(dialect.ddlGenerator().createSchema(SCHEMA.name(), true)); + execute(dialect.ddlGenerator().createTable(CUSTOMERS, custCols, null, true)); + + String qC = dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()); + + // INSERT — multi-row VALUES; runs through Hive's local execution. + execute("INSERT INTO " + qC + " VALUES (1, 'alice@example.com', 'Alice'), (2, 'bob@example.com', NULL)"); + + // ORDER BY through the dialect's ISNULL-based NULLS LAST emulation. + String orderItem = dialect.orderByGenerator() + .generateOrderItem(dialect.quoteIdentifier("NAME"), true, true, true).toString(); + assertThat(orderItem).contains("ISNULL"); + try (Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery( + "SELECT " + dialect.quoteIdentifier("NAME") + " FROM " + qC + " ORDER BY " + orderItem)) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("Alice"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isNull(); + assertThat(rs.next()).isFalse(); + } + + // Timestamp literal — Hive form cast( '...' as timestamp ). + StringBuilder ts = new StringBuilder(); + dialect.quoteTimestampLiteral(ts, "2024-01-15 10:30:00"); + try (Statement s = connection.createStatement(); ResultSet rs = s.executeQuery("SELECT " + ts)) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getTimestamp(1)).isEqualTo(Timestamp.valueOf("2024-01-15 10:30:00")); + } + + // VIEW. + execute(dialect.ddlGenerator().createView(VIEW, + "SELECT " + dialect.quoteIdentifier("NAME") + " FROM " + qC, false)); + try (Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery( + "SELECT COUNT(*) FROM " + dialect.quoteIdentifier(SCHEMA.name(), VIEW.name()))) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(2); + } + + // Inline VALUES through the dialect's generateInline form. + String inlineSql = dialect.sqlGenerator() + .generateInline(List.of("C1", "C2"), List.of("Varchar", "Numeric"), + List.of(new String[] { "x", "1" }, new String[] { "y", "2" })) + .toString(); + try (Statement s = connection.createStatement(); ResultSet rs = s.executeQuery(inlineSql)) { + assertThat(rs.next()).isTrue(); + assertThat(rs.next()).isTrue(); + assertThat(rs.next()).isFalse(); + } + + // DROP — reverse order; DROP SCHEMA ... CASCADE cleans up whatever remains. + execute(dialect.ddlGenerator().dropView(VIEW, true)); + execute(dialect.ddlGenerator().dropTable(CUSTOMERS, true)); + execute(dialect.ddlGenerator().dropSchema(SCHEMA.name(), true, true)); + } +} diff --git a/dialect/db/impala/pom.xml b/dialect/db/impala/pom.xml new file mode 100644 index 0000000..60de3ea --- /dev/null +++ b/dialect/db/impala/pom.xml @@ -0,0 +1,51 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db + ${revision} + ../pom.xml + + org.eclipse.daanse.sql.dialect.db.impala + Eclipse Daanse JDBC DB Dialect Impala + + + + org.slf4j + slf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.hive + ${revision} + + + biz.aQute.bnd + biz.aQute.bndlib + + + diff --git a/dialect/db/impala/src/main/java/org/eclipse/daanse/sql/dialect/db/impala/ImpalaDialect.java b/dialect/db/impala/src/main/java/org/eclipse/daanse/sql/dialect/db/impala/ImpalaDialect.java new file mode 100644 index 0000000..445caf2 --- /dev/null +++ b/dialect/db/impala/src/main/java/org/eclipse/daanse/sql/dialect/db/impala/ImpalaDialect.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.impala; + +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.db.common.DialectUtil; +import org.eclipse.daanse.sql.dialect.db.hive.HiveDialect; + +/** + * Dialect for Apache Impala. Connections are expected through the Apache + * Hive JDBC driver (HiveServer2 protocol, port 21050, {@code auth=noSasl}); + * Impala has no single-argument {@code ISNULL}, so NULL ordering uses the + * CASE WHEN sentinels of {@link #generateOrderItem} instead of Hive's form. + */ +public class ImpalaDialect extends HiveDialect { + + private static final String SUPPORTED_PRODUCT_NAME = "IMPALA"; + public static final String CAST = "cast("; + public static final String AS_STRING = " as string)"; + + /** JDBC-free constructor for SQL generation. Uses Impala backtick quoting. */ + public ImpalaDialect() { + super(DialectInitData.ansiDefaults().withQuoteIdentifierString("`")); + } + + /** Construct from a captured snapshot — the canonical entry point. */ + public ImpalaDialect(DialectInitData init) { + super(init); + } + + @Override + public StringBuilder generateOrderByNulls(CharSequence expr, boolean ascending, boolean collateNullsLast) { + StringBuilder sb = new StringBuilder(expr); + if (ascending) { + return sb.append(" ASC"); + } else { + return sb.append(" DESC"); + } + } + + @Override + public StringBuilder generateOrderItem(CharSequence expr, boolean nullable, boolean ascending, + boolean collateNullsLast) { + StringBuilder ret = new StringBuilder(); + + if (nullable && collateNullsLast) { + ret.append("CASE WHEN ").append(expr).append(" IS NULL THEN 1 ELSE 0 END, "); + } else { + ret.append("CASE WHEN ").append(expr).append(" IS NULL THEN 0 ELSE 1 END, "); + } + + if (ascending) { + ret.append(expr).append(" ASC"); + } else { + ret.append(expr).append(" DESC"); + } + + return ret; + } + + @Override + public boolean allowsMultipleCountDistinct() { + return false; + } + + @Override + public boolean requiresOrderByAlias() { + return false; + } + + @Override + public boolean supportsGroupByExpressions() { + return false; + } + + @Override + public boolean allowsSelectNotInGroupBy() { + return false; + } + + @Override + public StringBuilder generateInline(List columnNames, List columnTypes, List valueList) { + return generateInlineGeneric(columnNames, columnTypes, valueList, null, false); + } + + @Override + public void quoteStringLiteral(StringBuilder buf, String value) { + String quote = "\'"; + String s0 = value; + + if (s0.contains("\\")) { + // NOTE: no-op replacement, kept for behavioral fidelity with the + // legacy (Mondrian-era) dialect — backslashes are never doubled. + s0 = s0.replace("\\\\", "\\\\"); + } + if (s0.contains(quote)) { + s0 = s0.replace(quote, "\\\\" + quote); + } + + buf.append(quote); + + buf.append(s0); + + buf.append(quote); + } + + @Override + public boolean allowsRegularExpressionInWhereClause() { + return true; + } + + @Override + public Optional generateRegularExpression(String source, String javaRegex) { + try { + Pattern.compile(javaRegex); + } catch (PatternSyntaxException e) { + // Not a valid Java regex. Too risky to continue. + return Optional.empty(); + } + javaRegex = DialectUtil.cleanUnicodeAwareCaseFlag(javaRegex); + // We might have to use case-insensitive matching + StringBuilder mappedFlags = new StringBuilder(); + String[][] mapping = new String[][] { { "i", "i" } }; + javaRegex = extractEmbeddedFlags(javaRegex, mapping, mappedFlags); + boolean caseSensitive = true; + if (mappedFlags.toString().contains("i")) { + caseSensitive = false; + } + final Matcher escapeMatcher = DialectUtil.ESCAPE_PATTERN.matcher(javaRegex); + while (escapeMatcher.find()) { + javaRegex = javaRegex.replace(escapeMatcher.group(1), escapeMatcher.group(2)); + } + + final StringBuilder sb = new StringBuilder(); + // Now build the string. + sb.append(CAST); + sb.append(source); + sb.append(AS_STRING); + sb.append(" IS NOT NULL AND "); + if (caseSensitive) { + sb.append(CAST).append(source).append(AS_STRING); + } else { + sb.append("UPPER("); + sb.append(CAST).append(source).append(AS_STRING); + sb.append(")"); + } + sb.append(" REGEXP "); + if (caseSensitive) { + quoteStringLiteral(sb, javaRegex); + } else { + quoteStringLiteral(sb, javaRegex.toUpperCase()); + } + return Optional.of(sb.toString()); + } + + @Override + public boolean supportsDdl() { + return true; + } + + @Override + public String name() { + return SUPPORTED_PRODUCT_NAME.toLowerCase(); + } + +} diff --git a/dialect/db/impala/src/main/java/org/eclipse/daanse/sql/dialect/db/impala/ImpalaDialectFactory.java b/dialect/db/impala/src/main/java/org/eclipse/daanse/sql/dialect/db/impala/ImpalaDialectFactory.java new file mode 100644 index 0000000..45b36fc --- /dev/null +++ b/dialect/db/impala/src/main/java/org/eclipse/daanse/sql/dialect/db/impala/ImpalaDialectFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.impala; + +import java.util.function.Function; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.DialectName; +import org.eclipse.daanse.sql.dialect.db.common.AbstractDialectFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +@Component(service = DialectFactory.class, scope = ServiceScope.SINGLETON) +@DialectName("IMPALA") +public class ImpalaDialectFactory extends AbstractDialectFactory { + + @Override + public Function getConstructorFunction() { + return ImpalaDialect::new; + } + +} diff --git a/dialect/db/impala/src/main/java/org/eclipse/daanse/sql/dialect/db/impala/package-info.java b/dialect/db/impala/src/main/java/org/eclipse/daanse/sql/dialect/db/impala/package-info.java new file mode 100644 index 0000000..c52cf30 --- /dev/null +++ b/dialect/db/impala/src/main/java/org/eclipse/daanse/sql/dialect/db/impala/package-info.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.db.impala; diff --git a/dialect/db/impala/src/test/java/org/eclipse/daanse/sql/dialect/db/impala/ImpalaDialectQuotingPolicyTest.java b/dialect/db/impala/src/test/java/org/eclipse/daanse/sql/dialect/db/impala/ImpalaDialectQuotingPolicyTest.java new file mode 100644 index 0000000..cbf6574 --- /dev/null +++ b/dialect/db/impala/src/test/java/org/eclipse/daanse/sql/dialect/db/impala/ImpalaDialectQuotingPolicyTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.impala; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.junit.jupiter.api.Test; + +class ImpalaDialectQuotingPolicyTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "TEST"); + private static final TableReference EMP = new TableReference(Optional.of(S), "EMPLOYEES", + TableReference.TYPE_TABLE); + private static final TableReference DEPT = new TableReference(Optional.of(S), "DEPARTMENTS", + TableReference.TYPE_TABLE); + + private ImpalaDialect dialectNever() { + ImpalaDialect d = new ImpalaDialect(); + d.setQuotingPolicy(IdentifierQuotingPolicy.NEVER); + return d; + } + + @Test + void primaryKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addPrimaryKeyConstraint(EMP, "PK_EMP", List.of("ID"))) + .doesNotContain("`").contains("EMPLOYEES").contains("PK_EMP").contains("ID"); + } + + @Test + void uniqueConstraint_unquoted() { + assertThat(dialectNever().ddlGenerator().addUniqueConstraint(EMP, "UQ_EMP", List.of("EMAIL"))) + .doesNotContain("`").contains("EMPLOYEES").contains("UQ_EMP").contains("EMAIL"); + } + + @Test + void foreignKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addForeignKeyConstraint(EMP, "FK_EMP_DEPT", List.of("DEPT_ID"), DEPT, + List.of("ID"), "NO ACTION", "NO ACTION")).doesNotContain("`").contains("EMPLOYEES") + .contains("FK_EMP_DEPT").contains("DEPARTMENTS"); + } + + @Test + void renameTable_unquoted() { + assertThat(dialectNever().ddlGenerator().renameTable(EMP, "PERSON")).doesNotContain("`").contains("EMPLOYEES") + .contains("PERSON"); + } + + @Test + void createIndex_unquoted() { + assertThat(dialectNever().ddlGenerator().createIndex("IDX_EMP", EMP, List.of("NAME"), false, false)) + .doesNotContain("`").contains("EMPLOYEES").contains("IDX_EMP").contains("NAME"); + } + + @Test + void dropConstraint_unquoted() { + assertThat(dialectNever().ddlGenerator().dropConstraint(EMP, "PK_EMP", true)).doesNotContain("`") + .contains("EMPLOYEES").contains("PK_EMP"); + } +} diff --git a/dialect/db/impala/src/test/java/org/eclipse/daanse/sql/dialect/db/impala/ImpalaDialectTest.java b/dialect/db/impala/src/test/java/org/eclipse/daanse/sql/dialect/db/impala/ImpalaDialectTest.java new file mode 100644 index 0000000..854feb7 --- /dev/null +++ b/dialect/db/impala/src/test/java/org/eclipse/daanse/sql/dialect/db/impala/ImpalaDialectTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.impala; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ImpalaDialectTest { + + private ImpalaDialect dialect; + + @BeforeEach + void setUp() { + dialect = new ImpalaDialect(); + } + + @Test + void testName() { + assertEquals("impala", dialect.name()); + } + + @Test + void testQuoteIdentifierString() { + assertEquals("`", dialect.getQuoteIdentifierString()); + } + + @Test + void testAllowsRegularExpressionInWhereClause() { + assertTrue(dialect.allowsRegularExpressionInWhereClause()); + } + + @Test + void testGenerateRegularExpression_InvalidRegex() { + assertEquals(Optional.empty(), dialect.generateRegularExpression("table.column", "(a")); + } + + @Test + void testGenerateRegularExpression_CaseInsensitive() { + String sql = dialect.generateRegularExpression("table.column", "(?i)|(?u).*a.*").orElseThrow(); + assertTrue(sql.contains("UPPER")); + assertTrue(sql.contains("cast(table.column as string)")); + assertTrue(sql.contains("REGEXP")); + assertTrue(sql.contains("'.*A.*'")); + } + + @Test + void testGenerateRegularExpression_CaseSensitive() { + String sql = dialect.generateRegularExpression("table.column", ".*1.*").orElseThrow(); + assertFalse(sql.contains("UPPER")); + assertTrue(sql.contains("cast(table.column as string)")); + assertTrue(sql.contains("REGEXP")); + assertTrue(sql.contains("'.*1.*'")); + } + + @Test + void testGenerateOrderByNulls() { + assertEquals("column1 ASC", dialect.generateOrderByNulls("column1", true, true).toString()); + assertEquals("column1 DESC", dialect.generateOrderByNulls("column1", false, false).toString()); + } + + @Test + void testGenerateOrderByNulls_NeverEmitsIsnull() { + // Impala has no single-argument ISNULL(x) — Hive's emulation must not leak through. + for (boolean asc : new boolean[] { true, false }) { + for (boolean nullsLast : new boolean[] { true, false }) { + assertFalse(dialect.generateOrderByNulls("column1", asc, nullsLast).toString().contains("ISNULL")); + } + } + } + + @Test + void testGenerateOrderItem_NullableNullsLast() { + assertEquals("CASE WHEN column1 IS NULL THEN 1 ELSE 0 END, column1 ASC", + dialect.generateOrderItem("column1", true, true, true).toString()); + } + + @Test + void testGenerateOrderItem_NullableNullsFirst() { + assertEquals("CASE WHEN column1 IS NULL THEN 0 ELSE 1 END, column1 DESC", + dialect.generateOrderItem("column1", true, false, false).toString()); + } + + @Test + void testQuoteStringLiteral() { + StringBuilder buf = new StringBuilder(); + dialect.quoteStringLiteral(buf, "it's"); + assertEquals("'it\\\\'s'", buf.toString()); + } + + @Test + void testAllowsMultipleCountDistinct() { + assertFalse(dialect.allowsMultipleCountDistinct()); + } + + @Test + void testAllowsCompoundCountDistinct() { + assertTrue(dialect.allowsCompoundCountDistinct()); + } + + @Test + void testRequiresOrderByAlias() { + assertFalse(dialect.requiresOrderByAlias()); + } + + @Test + void testAllowsOrderByAlias() { + // Inherited explicitly from HiveDialect — must not flip with requiresOrderByAlias(). + assertTrue(dialect.allowsOrderByAlias()); + } + + @Test + void testRequiresAliasForFromQuery() { + assertTrue(dialect.requiresAliasForFromQuery()); + } + + @Test + void testSupportsGroupByExpressions() { + assertFalse(dialect.supportsGroupByExpressions()); + } + + @Test + void testAllowsSelectNotInGroupBy() { + assertFalse(dialect.allowsSelectNotInGroupBy()); + } + + @Test + void testAllowsJoinOn() { + assertFalse(dialect.allowsJoinOn()); + } + + @Test + void testSupportsDdl() { + assertTrue(dialect.supportsDdl()); + } +} diff --git a/dialect/db/impala/src/test/java/org/eclipse/daanse/sql/dialect/db/impala/integration/ServiceTest.java b/dialect/db/impala/src/test/java/org/eclipse/daanse/sql/dialect/db/impala/integration/ServiceTest.java new file mode 100644 index 0000000..75c0b88 --- /dev/null +++ b/dialect/db/impala/src/test/java/org/eclipse/daanse/sql/dialect/db/impala/integration/ServiceTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.impala.integration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.db.impala.ImpalaDialectFactory; +import org.junit.jupiter.api.Test; +import org.osgi.test.common.annotation.InjectService; + +class ServiceTest { + @Test + void serviceExists(@InjectService List dialects) throws Exception { + assertThat(dialects).isNotNull().isNotEmpty().anyMatch(ImpalaDialectFactory.class::isInstance); + } +} diff --git a/dialect/db/pom.xml b/dialect/db/pom.xml index 35a7ea1..f9d3e46 100644 --- a/dialect/db/pom.xml +++ b/dialect/db/pom.xml @@ -31,6 +31,8 @@ derby duckdb h2 + hive + impala mariadb mssqlserver mysql