From 9cb0a9b5126cf67968722635ab2c82298a479af9 Mon Sep 17 00:00:00 2001 From: 014-code <2402143478@qq.com> Date: Sat, 25 Jul 2026 20:20:12 +0800 Subject: [PATCH] DRILL-8551: Fix Informix JDBC table qualifiers --- .../exec/store/jdbc/JdbcDialectFactory.java | 17 ++- .../jdbc/informix/InformixJdbcDialect.java | 46 +++++++ .../informix/InformixJdbcImplementor.java | 63 +++++++++ .../informix/TestInformixJdbcDialect.java | 124 ++++++++++++++++++ 4 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 contrib/storage-jdbc/src/main/java/org/apache/drill/exec/store/jdbc/informix/InformixJdbcDialect.java create mode 100644 contrib/storage-jdbc/src/main/java/org/apache/drill/exec/store/jdbc/informix/InformixJdbcImplementor.java create mode 100644 contrib/storage-jdbc/src/test/java/org/apache/drill/exec/store/jdbc/informix/TestInformixJdbcDialect.java diff --git a/contrib/storage-jdbc/src/main/java/org/apache/drill/exec/store/jdbc/JdbcDialectFactory.java b/contrib/storage-jdbc/src/main/java/org/apache/drill/exec/store/jdbc/JdbcDialectFactory.java index b3eaef16ea9..7e4b8e9eb75 100644 --- a/contrib/storage-jdbc/src/main/java/org/apache/drill/exec/store/jdbc/JdbcDialectFactory.java +++ b/contrib/storage-jdbc/src/main/java/org/apache/drill/exec/store/jdbc/JdbcDialectFactory.java @@ -18,12 +18,16 @@ package org.apache.drill.exec.store.jdbc; import org.apache.calcite.sql.SqlDialect; +import org.apache.calcite.sql.dialect.InformixSqlDialect; import org.apache.drill.exec.store.jdbc.clickhouse.ClickhouseJdbcDialect; +import org.apache.drill.exec.store.jdbc.informix.InformixJdbcDialect; import java.time.Duration; +import java.util.Locale; public class JdbcDialectFactory { public static final String JDBC_CLICKHOUSE_PREFIX = "jdbc:clickhouse"; + public static final String JDBC_INFORMIX_PREFIX = "jdbc:informix"; public static final int CACHE_SIZE = 100; public static final Duration CACHE_TTL = Duration.ofHours(1); private volatile JdbcDialect jdbcDialect; @@ -38,9 +42,16 @@ public JdbcDialect getJdbcDialect(JdbcStoragePlugin plugin, SqlDialect dialect) synchronized (this) { jd = jdbcDialect; if (jd == null) { - jd = plugin.getConfig().getUrl().startsWith(JDBC_CLICKHOUSE_PREFIX) - ? new ClickhouseJdbcDialect(plugin, dialect) - : new DefaultJdbcDialect(plugin, dialect); + String url = plugin.getConfig().getUrl(); + String normalizedUrl = url == null ? null : url.toLowerCase(Locale.ROOT); + if (normalizedUrl != null && normalizedUrl.startsWith(JDBC_CLICKHOUSE_PREFIX)) { + jd = new ClickhouseJdbcDialect(plugin, dialect); + } else if (dialect instanceof InformixSqlDialect + || normalizedUrl != null && normalizedUrl.startsWith(JDBC_INFORMIX_PREFIX)) { + jd = new InformixJdbcDialect(plugin, dialect); + } else { + jd = new DefaultJdbcDialect(plugin, dialect); + } jdbcDialect = jd; } } diff --git a/contrib/storage-jdbc/src/main/java/org/apache/drill/exec/store/jdbc/informix/InformixJdbcDialect.java b/contrib/storage-jdbc/src/main/java/org/apache/drill/exec/store/jdbc/informix/InformixJdbcDialect.java new file mode 100644 index 00000000000..4bd69269bd0 --- /dev/null +++ b/contrib/storage-jdbc/src/main/java/org/apache/drill/exec/store/jdbc/informix/InformixJdbcDialect.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.drill.exec.store.jdbc.informix; + +import org.apache.calcite.adapter.java.JavaTypeFactory; +import org.apache.calcite.adapter.jdbc.JdbcImplementor; +import org.apache.calcite.plan.RelOptCluster; +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.sql.SqlDialect; +import org.apache.drill.exec.store.SubsetRemover; +import org.apache.drill.exec.store.jdbc.DefaultJdbcDialect; +import org.apache.drill.exec.store.jdbc.JdbcStoragePlugin; + +public class InformixJdbcDialect extends DefaultJdbcDialect { + + private final SqlDialect dialect; + + public InformixJdbcDialect(JdbcStoragePlugin plugin, SqlDialect dialect) { + super(plugin, dialect); + this.dialect = dialect; + } + + @Override + public String generateSql(RelOptCluster cluster, RelNode input) { + final JdbcImplementor jdbcImplementor = new InformixJdbcImplementor(dialect, + (JavaTypeFactory) cluster.getTypeFactory()); + final JdbcImplementor.Result result = jdbcImplementor.visitRoot( + input.accept(SubsetRemover.INSTANCE)); + return result.asStatement().toSqlString(dialect).getSql(); + } +} diff --git a/contrib/storage-jdbc/src/main/java/org/apache/drill/exec/store/jdbc/informix/InformixJdbcImplementor.java b/contrib/storage-jdbc/src/main/java/org/apache/drill/exec/store/jdbc/informix/InformixJdbcImplementor.java new file mode 100644 index 00000000000..1524e3a3519 --- /dev/null +++ b/contrib/storage-jdbc/src/main/java/org/apache/drill/exec/store/jdbc/informix/InformixJdbcImplementor.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.drill.exec.store.jdbc.informix; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.calcite.adapter.java.JavaTypeFactory; +import org.apache.calcite.adapter.jdbc.JdbcImplementor; +import org.apache.calcite.adapter.jdbc.JdbcTable; +import org.apache.calcite.plan.RelOptTable; +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.rel.core.TableScan; +import org.apache.calcite.sql.SqlDialect; +import org.apache.calcite.sql.SqlIdentifier; +import org.apache.calcite.sql.parser.SqlParserPos; + +import com.google.common.collect.ImmutableList; + +import static java.util.Objects.requireNonNull; + +public class InformixJdbcImplementor extends JdbcImplementor { + public InformixJdbcImplementor(SqlDialect dialect, JavaTypeFactory typeFactory) { + super(dialect, typeFactory); + } + + @Override + public Result visit(TableScan scan) { + SqlIdentifier sqlIdentifier = getSqlTargetTable(scan); + return result(sqlIdentifier, ImmutableList.of(Clause.FROM), scan, null); + } + + static SqlIdentifier getSqlTargetTable(RelNode e) { + RelOptTable table = requireNonNull(e.getTable()); + return table.maybeUnwrap(JdbcTable.class) + .map(jdbcTable -> { + List names = new ArrayList<>(2); + // Informix JDBC reports the connected database as a catalog, but + // Informix SQL does not accept it as a dot-qualified table prefix. + if (jdbcTable.jdbcSchemaName != null) { + names.add(jdbcTable.jdbcSchemaName); + } + names.add(jdbcTable.jdbcTableName); + return new SqlIdentifier(names, SqlParserPos.ZERO); + }) + .orElseGet(() -> new SqlIdentifier(table.getQualifiedName(), SqlParserPos.ZERO)); + } +} diff --git a/contrib/storage-jdbc/src/test/java/org/apache/drill/exec/store/jdbc/informix/TestInformixJdbcDialect.java b/contrib/storage-jdbc/src/test/java/org/apache/drill/exec/store/jdbc/informix/TestInformixJdbcDialect.java new file mode 100644 index 00000000000..2315a759b74 --- /dev/null +++ b/contrib/storage-jdbc/src/test/java/org/apache/drill/exec/store/jdbc/informix/TestInformixJdbcDialect.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.drill.exec.store.jdbc.informix; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Constructor; +import java.util.Optional; + +import javax.sql.DataSource; + +import org.apache.calcite.adapter.jdbc.JdbcSchema; +import org.apache.calcite.adapter.jdbc.JdbcTable; +import org.apache.calcite.plan.RelOptTable; +import org.apache.calcite.rel.core.TableScan; +import org.apache.calcite.schema.Schema; +import org.apache.calcite.sql.SqlDialect; +import org.apache.calcite.sql.dialect.InformixSqlDialect; +import org.apache.drill.exec.store.jdbc.JdbcDialect; +import org.apache.drill.exec.store.jdbc.JdbcDialectFactory; +import org.apache.drill.exec.store.jdbc.JdbcStorageConfig; +import org.apache.drill.exec.store.jdbc.JdbcStoragePlugin; +import org.junit.Test; + +public class TestInformixJdbcDialect { + + @Test + public void dropsCatalogFromTargetTable() throws Exception { + JdbcTable jdbcTable = newJdbcTable("sample_database", null, "sample_table"); + TableScan scan = mock(TableScan.class); + RelOptTable relOptTable = mock(RelOptTable.class); + when(scan.getTable()).thenReturn(relOptTable); + when(relOptTable.maybeUnwrap(JdbcTable.class)).thenReturn(Optional.of(jdbcTable)); + + String sql = InformixJdbcImplementor.getSqlTargetTable(scan) + .toSqlString(InformixSqlDialect.DEFAULT) + .getSql(); + + assertEquals("sample_table", sql); + } + + @Test + public void keepsSchemaWhenPresent() throws Exception { + JdbcTable jdbcTable = newJdbcTable("sample_database", "sample_owner", "sample_table"); + TableScan scan = mock(TableScan.class); + RelOptTable relOptTable = mock(RelOptTable.class); + when(scan.getTable()).thenReturn(relOptTable); + when(relOptTable.maybeUnwrap(JdbcTable.class)).thenReturn(Optional.of(jdbcTable)); + + String sql = InformixJdbcImplementor.getSqlTargetTable(scan) + .toSqlString(InformixSqlDialect.DEFAULT) + .getSql(); + + assertEquals("sample_owner.sample_table", sql); + } + + @Test + public void factorySelectsInformixDialect() { + JdbcStoragePlugin plugin = mock(JdbcStoragePlugin.class); + when(plugin.getConfig()).thenReturn(newJdbcStorageConfig("jdbc:other://localhost")); + + JdbcDialect dialect = new JdbcDialectFactory().getJdbcDialect(plugin, InformixSqlDialect.DEFAULT); + assertTrue(dialect instanceof InformixJdbcDialect); + } + + @Test + public void factorySelectsInformixDialectFromUrl() { + JdbcStoragePlugin plugin = mock(JdbcStoragePlugin.class); + when(plugin.getConfig()).thenReturn( + newJdbcStorageConfig("jdbc:informix-sqli://localhost:1526/sample_database")); + + JdbcDialect dialect = new JdbcDialectFactory().getJdbcDialect( + plugin, SqlDialect.DatabaseProduct.UNKNOWN.getDialect()); + assertTrue(dialect instanceof InformixJdbcDialect); + } + + private static JdbcStorageConfig newJdbcStorageConfig(String url) { + JdbcStorageConfig config = new JdbcStorageConfig( + "com.informix.jdbc.IfxDriver", + url, + null, + null, + true, + false, + null, + null, + null, + 0 + ); + return config; + } + + private static JdbcTable newJdbcTable(String catalog, String schema, String table) throws Exception { + DataSource dataSource = mock(DataSource.class); + JdbcSchema jdbcSchema = new JdbcSchema(dataSource, InformixSqlDialect.DEFAULT, null, catalog, schema); + Constructor ctor = JdbcTable.class.getDeclaredConstructor( + JdbcSchema.class, + String.class, + String.class, + String.class, + Schema.TableType.class + ); + ctor.setAccessible(true); + return ctor.newInstance(jdbcSchema, catalog, schema, table, Schema.TableType.TABLE); + } +}