Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions chat2db-community-client/src/constants/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ export const databaseMap: {
name: 'Hive',
code: DatabaseTypeCode.HIVE,
icon: 'icon-colourful-HIVE',
supportDatabase: false,
supportSchema: true,
supportDatabase: true,
supportSchema: false,
},
[DatabaseTypeCode.KINGBASE]: {
name: 'Kingbase',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ assert.deepEqual(getDatabaseSupport(DatabaseTypeCode.MYSQL), {
supportDatabase: true,
supportSchema: false,
});
assert.deepEqual(getDatabaseSupport(DatabaseTypeCode.HIVE), {
supportDatabase: true,
supportSchema: false,
});
assert.deepEqual(getDatabaseSupport(DatabaseTypeCode.ORACLE), {
supportDatabase: false,
supportSchema: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ private void sortSchema(List<Schema> schemas, Connection connection) {
try {
ulr = connection.getMetaData().getURL();
} catch (SQLException e) {
throw new IllegalStateException("Failed to read JDBC URL while sorting schemas", e);
log.debug("JDBC driver does not expose its URL; skip schema ordering", e);
return;
}
int num = -1;
for (int i = 0; i < schemas.size(); i++) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package ai.chat2db.community.domain.core.impl.db;

import ai.chat2db.community.domain.api.model.metadata.Schema;
import org.junit.jupiter.api.Test;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;

class DbDatabaseServiceImplTest {

@Test
void unsupportedJdbcUrlDoesNotDiscardSchemas() throws Throwable {
List<Schema> schemas = new ArrayList<>();
schemas.add(Schema.builder().name("analytics").build());
schemas.add(Schema.builder().name("default").build());

invokeSortSchema(new DbDatabaseServiceImpl(), schemas, connectionWithoutUrl());

assertEquals(List.of("analytics", "default"), schemas.stream().map(Schema::getName).toList());
}

private static void invokeSortSchema(DbDatabaseServiceImpl service, List<Schema> schemas,
Connection connection) throws Throwable {
Method method = DbDatabaseServiceImpl.class.getDeclaredMethod("sortSchema", List.class, Connection.class);
method.setAccessible(true);
try {
method.invoke(service, schemas, connection);
} catch (InvocationTargetException exception) {
throw exception.getCause();
}
}

private static Connection connectionWithoutUrl() {
DatabaseMetaData metaData = proxy(DatabaseMetaData.class, (proxy, method, args) -> {
if ("getURL".equals(method.getName())) {
throw new SQLException("Method not supported");
}
return defaultValue(method.getReturnType());
});
return proxy(Connection.class, (proxy, method, args) -> {
if ("getMetaData".equals(method.getName())) {
return metaData;
}
return defaultValue(method.getReturnType());
});
}

@SuppressWarnings("unchecked")
private static <T> T proxy(Class<T> type, InvocationHandler handler) {
return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handler);
}

private static Object defaultValue(Class<?> type) {
if (!type.isPrimitive()) {
return null;
}
if (type == boolean.class) {
return false;
}
if (type == char.class) {
return '\0';
}
if (type == byte.class) {
return (byte) 0;
}
if (type == short.class) {
return (short) 0;
}
if (type == int.class) {
return 0;
}
if (type == long.class) {
return 0L;
}
if (type == float.class) {
return 0F;
}
return 0D;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import ai.chat2db.spi.model.value.*;
import ai.chat2db.community.domain.api.model.view.*;
import ai.chat2db.spi.DefaultSQLExecutor;
import com.google.common.collect.Lists;
import jakarta.validation.constraints.NotEmpty;
import org.apache.commons.lang3.StringUtils;

Expand All @@ -38,27 +37,30 @@ public ISQLIdentifierProcessor getSQLIdentifierProcessor() {

@Override
public List<Database> databases(Connection connection) {
return Lists.newArrayList();
}

@Override
public List<Schema> schemas(Connection connection, String databaseName) {
List<Schema> schemas = new ArrayList<>();
return DefaultSQLExecutor.getInstance().execute(connection,SQL_SHOW_DATABASES, resultSet -> {
List<Database> databases = new ArrayList<>();
return DefaultSQLExecutor.getInstance().execute(connection, SQL_SHOW_DATABASES, resultSet -> {
try {
while (resultSet.next()) {
String schenaNane = resultSet.getString("database_name");
Schema schema = new Schema();
schema.setName(schenaNane);
schemas.add(schema);
String databaseName = resultSet.getString("database_name");
if (StringUtils.isBlank(databaseName)) {
continue;
}
Database database = new Database();
database.setName(databaseName);
databases.add(database);
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return schemas;
return databases;
});
}

@Override
public List<Schema> schemas(Connection connection, String databaseName) {
return Collections.emptyList();
}

@Override
public String tableDDL(Connection connection, @NotEmpty String databaseName, String schemaName,
@NotEmpty String tableName) {
Expand All @@ -81,7 +83,7 @@ public String tableDDL(Connection connection, @NotEmpty String databaseName, Str

@Override
public String getMetaDataName(String... names) {
return Arrays.stream(names).skip(1).filter(name -> StringUtils.isNotBlank(name))
return Arrays.stream(names).filter(name -> StringUtils.isNotBlank(name))
.map(HiveIdentifierProcessor.INSTANCE::quoteIdentifierAlways)
.collect(Collectors.joining("."));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"dbType": "HIVE",
"supportDatabase": false,
"supportSchema": true,
"supportDatabase": true,
"supportSchema": false,
"driverConfigList": [
{
"url": "jdbc:hive2://localhost:10000/",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ void dropTableQuotesIdentifier() {
void metaDataFormatAndNameQuoteIdentifiers() {
assertEquals("`a``b`", HiveMetaData.format("a`b"));
assertEquals("`a``; DROP TABLE b; --`", HiveMetaData.format("a`; DROP TABLE b; --"));
assertEquals("`db`.`a``b`", new HiveMetaData().getMetaDataName("ignored", "db", "a`b"));
assertEquals("`db`.`a``b`", new HiveMetaData().getQualifiedTableName("db", null, "a`b"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package ai.chat2db.plugin.hive;

import ai.chat2db.community.domain.api.config.DBConfig;
import ai.chat2db.community.domain.api.model.metadata.Database;
import org.junit.jupiter.api.Test;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class HiveMetaDataTest {

@Test
void configExposesHiveNamespacesAsDatabases() {
DBConfig config = new HivePlugin().getDBConfig();

assertTrue(config.isSupportDatabase());
assertFalse(config.isSupportSchema());
}

@Test
void databasesReadsShowDatabasesResult() {
AtomicReference<String> executedSql = new AtomicReference<>();
Connection connection = connectionWithDatabaseRows(List.of("analytics", "default"), executedSql);

List<Database> databases = new HiveMetaData().databases(connection);

assertEquals("show databases", executedSql.get());
assertEquals(List.of("analytics", "default"), databases.stream().map(Database::getName).toList());
}

@Test
void schemasAreEmptyBecauseHiveHasNoSeparateSchemaNamespace() {
assertTrue(new HiveMetaData().schemas(null, "analytics").isEmpty());
}

@Test
void qualifiedTableNameKeepsHiveDatabasePrefix() {
HiveMetaData metaData = new HiveMetaData();

assertEquals("`analytics`.`events`", metaData.getQualifiedTableName("analytics", null, "events"));
assertEquals("`events`", metaData.getQualifiedTableName(null, null, "events"));
}

private static Connection connectionWithDatabaseRows(List<String> names, AtomicReference<String> executedSql) {
AtomicInteger row = new AtomicInteger(-1);
ResultSet resultSet = proxy(ResultSet.class, (proxy, method, args) -> switch (method.getName()) {
case "next" -> row.incrementAndGet() < names.size();
case "getString" -> names.get(row.get());
case "close" -> null;
default -> defaultValue(method.getReturnType());
});
PreparedStatement statement = proxy(PreparedStatement.class, (proxy, method, args) -> switch (method.getName()) {
case "execute" -> true;
case "getResultSet" -> resultSet;
case "close" -> null;
default -> defaultValue(method.getReturnType());
});
return proxy(Connection.class, (proxy, method, args) -> {
if ("prepareStatement".equals(method.getName())) {
executedSql.set((String) args[0]);
return statement;
}
return defaultValue(method.getReturnType());
});
}

@SuppressWarnings("unchecked")
private static <T> T proxy(Class<T> type, InvocationHandler handler) {
return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handler);
}

private static Object defaultValue(Class<?> type) {
if (!type.isPrimitive()) {
return null;
}
if (type == boolean.class) {
return false;
}
if (type == char.class) {
return '\0';
}
if (type == byte.class) {
return (byte) 0;
}
if (type == short.class) {
return (short) 0;
}
if (type == int.class) {
return 0;
}
if (type == long.class) {
return 0L;
}
if (type == float.class) {
return 0F;
}
return 0D;
}
}
Loading