diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 61b492976..cca88c92a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,6 +62,7 @@ jobs: java-version: "17" distribution: "temurin" java-package: "jre" + architecture: "x64" # Install jre MacOS arm64 - name: Install Jre MacOS arm64 @@ -86,7 +87,7 @@ jobs: - name: Enable tls1 if: ${{ runner.os == 'Windows' }} run: | - # sed -i '' "s/\(^jdk.tls.disabledAlgorithms=\)\(.*\)\( TLSv1, TLSv1.1,\)\(.*\)/\1\2\4/" "${{ env.JAVA_HOME }}/conf/security/java.security" + # sed -i '' "s/\(^jdk.tls.disabledAlgorithms=\)\(.*\)\( TLSv1, TLSv1.1,\)\(.*\)/\1\2\4/" "${{ env.JAVA_HOME }}\conf\security\java.security" $filePath = "${{ env.JAVA_HOME }}\conf\security\java.security" $content = Get-Content $filePath -Raw $updatedContent = $content -replace '^(jdk.tls.disabledAlgorithms=)(.*)( TLSv1, TLSv1.1,)(.*)', '$1$2$4' @@ -210,7 +211,7 @@ jobs: run: | xcrun notarytool store-credentials "Chat2DB" --apple-id "${{secrets.MAC_APPLE_ID}}" --password "${{secrets.MAC_APPLE_PASSWORD}}" --team-id "${{secrets.MAC_TEAM_ID}}" xcrun notarytool submit chat2db-client/release/Chat2DB-${{ steps.chat2db_version.outputs.substring }}.dmg --keychain-profile "Chat2DB" - + # macos arm64 - name: Build/release Electron app for MacOS arm64 if: ${{ runner.os == 'macOS' && matrix.arch == 'arm64' }} @@ -346,4 +347,4 @@ jobs: { "title": "Linux-test-打包完成通知", "text": "# Linux-test-打包完成通知 \n ![bang](https://oss.sqlgpt.cn/static/happy100.jpg) \n ### 任务id:[${{ github.run_id }}](https://github.com/chat2db/Chat2DB/actions/runs/${{ github.run_id }}) \n ### Linux下载地址:[https://oss.sqlgpt.cn/release/${{ steps.chat2db_version.outputs.substring }}/Chat2DB-${{ steps.chat2db_version.outputs.substring }}.AppImage](https://oss.sqlgpt.cn/release/${{ steps.chat2db_version.outputs.substring }}/Chat2DB-${{ steps.chat2db_version.outputs.substring }}.AppImage)" - } + } \ No newline at end of file diff --git a/chat2db-client/src/components/ConnectionEdit/config/dataSource.ts b/chat2db-client/src/components/ConnectionEdit/config/dataSource.ts index c95ca70ef..28f51acd7 100644 --- a/chat2db-client/src/components/ConnectionEdit/config/dataSource.ts +++ b/chat2db-client/src/components/ConnectionEdit/config/dataSource.ts @@ -2104,4 +2104,43 @@ export const dataSourceFormConfigs: IConnectionConfig[] = [ }, ssh: sshConfig, }, + // DUCKDB + { + type: DatabaseTypeCode.DUCKDB, + baseInfo: { + items: [ + { + defaultValue: '@localhost', + inputType: InputType.INPUT, + labelNameCN: '名称', + labelNameEN: 'Name', + name: 'alias', + required: true, + }, + envItem, + { + defaultValue: 'localhost', + inputType: InputType.INPUT, + labelNameCN: '路径', + labelNameEN: 'Host', + name: 'host', + required: true, + styles: { + width: '70%', + }, + }, + { + defaultValue: 'jdbc:duckdb:{filePath}', + inputType: InputType.INPUT, + labelNameCN: 'URL', + labelNameEN: 'URL', + name: 'url', + required: true, + }, + ], + pattern: /jdbc:duckdb:\/\/(\w+)/, + template: 'jdbc:duckdb://{host}', + }, + ssh: sshConfig, + }, ]; diff --git a/chat2db-client/src/components/ConnectionEdit/config/enum.ts b/chat2db-client/src/components/ConnectionEdit/config/enum.ts index 6c0e425cf..e66e37e93 100644 --- a/chat2db-client/src/components/ConnectionEdit/config/enum.ts +++ b/chat2db-client/src/components/ConnectionEdit/config/enum.ts @@ -2,6 +2,7 @@ export enum InputType { INPUT = 'input', PASSWORD = 'password', SELECT = 'select', + FILE = 'file', } export enum AuthenticationType { diff --git a/chat2db-client/src/constants/common.ts b/chat2db-client/src/constants/common.ts index 44824008d..8ea26bcbf 100644 --- a/chat2db-client/src/constants/common.ts +++ b/chat2db-client/src/constants/common.ts @@ -16,6 +16,7 @@ export enum DatabaseTypeCode { HIVE = 'HIVE', KINGBASE = 'KINGBASE', TIMEPLUS = 'TIMEPLUS', + DUCKDB = 'DUCKDB', } export enum ConsoleStatus { diff --git a/chat2db-client/src/constants/database.ts b/chat2db-client/src/constants/database.ts index e586cb883..7f174ed5e 100644 --- a/chat2db-client/src/constants/database.ts +++ b/chat2db-client/src/constants/database.ts @@ -125,6 +125,13 @@ export const databaseMap: { // port: 8123, icon: '\ue8f4', }, + [DatabaseTypeCode.DUCKDB]: { + name: 'DuckDB', + img: moreDBLogo, + code: DatabaseTypeCode.DUCKDB, + // port: 8123, + icon: '\ue8f4', + }, // [DatabaseTypeCode.REDIS]: { // name: 'Redis', // img: moreDBLogo, diff --git a/chat2db-server/chat2db-plugins/chat2db-clickhouse/src/main/java/ai/chat2db/plugin/clickhouse/builder/ClickHouseSqlBuilder.java b/chat2db-server/chat2db-plugins/chat2db-clickhouse/src/main/java/ai/chat2db/plugin/clickhouse/builder/ClickHouseSqlBuilder.java index 9f313c27c..0eef415af 100644 --- a/chat2db-server/chat2db-plugins/chat2db-clickhouse/src/main/java/ai/chat2db/plugin/clickhouse/builder/ClickHouseSqlBuilder.java +++ b/chat2db-server/chat2db-plugins/chat2db-clickhouse/src/main/java/ai/chat2db/plugin/clickhouse/builder/ClickHouseSqlBuilder.java @@ -10,6 +10,8 @@ import ai.chat2db.spi.model.TableIndex; import org.apache.commons.lang3.StringUtils; +import java.util.List; + public class ClickHouseSqlBuilder extends DefaultSqlBuilder { @Override diff --git a/chat2db-server/chat2db-plugins/chat2db-duckdb/pom.xml b/chat2db-server/chat2db-plugins/chat2db-duckdb/pom.xml new file mode 100644 index 000000000..7eddf5958 --- /dev/null +++ b/chat2db-server/chat2db-plugins/chat2db-duckdb/pom.xml @@ -0,0 +1,35 @@ + + + + + ai.chat2db + chat2db-plugins + ${revision} + ../pom.xml + + 4.0.0 + chat2db-duckdb + + + + ai.chat2db + chat2db-spi + + + + + + src/main/java + + + **/*.json + + + + src/main/resources + + + + \ No newline at end of file diff --git a/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/DuckDBCommandExecutor.java b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/DuckDBCommandExecutor.java new file mode 100644 index 000000000..faf5a0bd7 --- /dev/null +++ b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/DuckDBCommandExecutor.java @@ -0,0 +1,17 @@ +package ai.chat2db.plugin.duckdb; + +import ai.chat2db.spi.model.Command; +import ai.chat2db.spi.model.ExecuteResult; +import ai.chat2db.spi.sql.SQLExecutor; + +import java.util.List; + +public class DuckDBCommandExecutor extends SQLExecutor { + + @Override + public List executeSelectTable(Command command) { + String sql = "select * from " +command.getSchemaName() + "." + command.getTableName(); + command.setScript(sql); + return execute(command); + } +} diff --git a/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/DuckDBManage.java b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/DuckDBManage.java new file mode 100644 index 000000000..b4c117534 --- /dev/null +++ b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/DuckDBManage.java @@ -0,0 +1,253 @@ +package ai.chat2db.plugin.duckdb; + +import ai.chat2db.spi.DBManage; +import ai.chat2db.spi.jdbc.DefaultDBManage; +import ai.chat2db.spi.model.AsyncContext; +import ai.chat2db.spi.model.Function; +import ai.chat2db.spi.model.Procedure; +import ai.chat2db.spi.sql.SQLExecutor; +import cn.hutool.core.date.DateUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Date; + +import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN; + +@Slf4j +public class DuckDBManage extends DefaultDBManage implements DBManage { + + private static String PROCEDURE_SQL = "SELECT COUNT(*) FROM INFORMATION_SCHEMA.ROUTINES " + + "WHERE ROUTINE_SCHEMA = '%s' AND ROUTINE_NAME = '%s' AND ROUTINE_TYPE = 'PROCEDURE'"; + + @Override + public void exportDatabase(Connection connection, String databaseName, String schemaName, AsyncContext asyncContext) throws SQLException { + asyncContext.write(String.format(EXPORT_TITLE, DateUtil.format(new Date(), NORM_DATETIME_PATTERN))); + exportTables(connection, databaseName, schemaName, asyncContext); + asyncContext.setProgress(50); + exportViews(connection, databaseName, asyncContext); + asyncContext.setProgress(60); + exportProcedures(connection, asyncContext); + asyncContext.setProgress(70); + exportTriggers(connection, asyncContext); + asyncContext.setProgress(90); + exportFunctions(connection, databaseName, asyncContext); + asyncContext.finish(); + } + + private void exportFunctions(Connection connection, String databaseName, AsyncContext asyncContext) throws SQLException { + try (ResultSet resultSet = connection.getMetaData().getFunctions(databaseName, null, null)) { + while (resultSet.next()) { + exportFunction(connection, resultSet.getString("FUNCTION_NAME"), asyncContext); + } + + } + } + + private void exportFunction(Connection connection, String functionName, AsyncContext asyncContext) throws SQLException { + String sql = String.format("SHOW CREATE FUNCTION %s;", functionName); + try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) { + if (resultSet.next()) { + asyncContext.write(String.format(FUNCTION_TITLE, functionName)); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("DROP FUNCTION IF EXISTS ").append(functionName).append(";").append("\n"); + + sqlBuilder.append("delimiter ;;").append("\n").append(resultSet.getString("Create Function")).append(";;") + .append("\n").append("delimiter ;").append("\n\n"); + asyncContext.write(sqlBuilder.toString()); + } + } + } + + private void exportTables(Connection connection, String databaseName, String schemaName, AsyncContext asyncContext) throws SQLException { + asyncContext.write("SET FOREIGN_KEY_CHECKS=0;"); + try (ResultSet resultSet = connection.getMetaData().getTables(databaseName, null, null, new String[]{"TABLE", "SYSTEM TABLE"})) { + while (resultSet.next()) { + String tableName = resultSet.getString("TABLE_NAME"); + exportTable(connection, databaseName, schemaName, tableName, asyncContext); + } + } + asyncContext.write("SET FOREIGN_KEY_CHECKS=1;"); + } + + + public void exportTable(Connection connection, String databaseName, String schemaName, String tableName, AsyncContext asyncContext) throws SQLException { + String sql = String.format("show create table %s ", tableName); + try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) { + if (resultSet.next()) { + StringBuilder sqlBuilder = new StringBuilder(); + asyncContext.write(String.format(TABLE_TITLE, tableName)); + sqlBuilder.append("DROP TABLE IF EXISTS ").append(format(tableName)).append(";").append("\n") + .append(resultSet.getString("Create Table")).append(";").append("\n"); + asyncContext.write(sqlBuilder.toString()); + if (asyncContext.isContainsData()) { + exportTableData(connection, databaseName, schemaName, tableName, asyncContext); + } + } + } catch (Exception e) { + log.error("export table error", e); + asyncContext.error(String.format("export table %s error:%s", tableName, e.getMessage())); + } + } + + + private void exportViews(Connection connection, String databaseName, AsyncContext asyncContext) throws SQLException { + try (ResultSet resultSet = connection.getMetaData().getTables(databaseName, null, null, new String[]{"VIEW"})) { + while (resultSet.next()) { + exportView(connection, resultSet.getString("TABLE_NAME"), asyncContext); + } + } + } + + private void exportView(Connection connection, String viewName, AsyncContext asyncContext) throws SQLException { + String sql = String.format("show create view %s ", viewName); + try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) { + if (resultSet.next()) { + asyncContext.write(String.format(VIEW_TITLE, viewName)); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("DROP VIEW IF EXISTS ").append(format(viewName)).append(";").append("\n") + .append(resultSet.getString("Create View")).append(";").append("\n\n"); + asyncContext.write(sqlBuilder.toString()); + } + } + } + + private void exportProcedures(Connection connection, AsyncContext asyncContext) throws SQLException { + String sql = "SHOW PROCEDURE STATUS WHERE Db = DATABASE()"; + try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) { + while (resultSet.next()) { + exportProcedure(connection, resultSet.getString("Name"), asyncContext); + } + } + } + + private void exportProcedure(Connection connection, String procedureName, AsyncContext asyncContext) throws SQLException { + String sql = String.format("show create procedure %s ", procedureName); + try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) { + if (resultSet.next()) { + asyncContext.write(String.format(PROCEDURE_TITLE, procedureName)); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("DROP PROCEDURE IF EXISTS ").append(format(procedureName)).append(";").append("\n") + .append("delimiter ;;").append("\n").append(resultSet.getString("Create Procedure")).append(";;") + .append("\n").append("delimiter ;").append("\n\n"); + asyncContext.write(sqlBuilder.toString()); + } + } + } + + private void exportTriggers(Connection connection, AsyncContext asyncContext) throws SQLException { + String sql = "SHOW TRIGGERS"; + try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) { + while (resultSet.next()) { + String triggerName = resultSet.getString("Trigger"); + exportTrigger(connection, triggerName, asyncContext); + } + } + } + + private void exportTrigger(Connection connection, String triggerName, AsyncContext asyncContext) throws SQLException { + String sql = String.format("show create trigger %s ", triggerName); + try (ResultSet resultSet = connection.createStatement().executeQuery(sql)) { + if (resultSet.next()) { + asyncContext.write(String.format(TRIGGER_TITLE, triggerName)); + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("DROP TRIGGER IF EXISTS ").append(format(triggerName)).append(";").append("\n") + .append("delimiter ;;").append("\n").append(resultSet.getString("SQL Original Statement")).append(";;") + .append("\n").append("delimiter ;").append("\n\n"); + asyncContext.write(sqlBuilder.toString()); + } + } + } + + @Override + public void updateProcedure(Connection connection, String databaseName, String schemaName, Procedure procedure) throws SQLException { + try { + connection.setAutoCommit(false); + String procedureBody = procedure.getProcedureBody(); + if (procedureBody == null || !procedureBody.trim().toUpperCase().startsWith("CREATE")) { + throw new IllegalArgumentException("No CREATE statement found."); + } + + String procedureNewName = getSchemaOrProcedureName(procedureBody, databaseName, procedure); + if (!procedureNewName.equals(procedure.getProcedureName())) { + procedureBody = procedureBody.replace(procedure.getProcedureName(), procedureNewName); + } + String checkProcedureSQL = String.format(PROCEDURE_SQL, databaseName, procedure.getProcedureName()); + SQLExecutor.getInstance().execute(connection, checkProcedureSQL, resultSet -> { + try { + if (resultSet.next()) { + int count = resultSet.getInt(1); + if (count >= 1) { + throw new Exception("Procedure already exists"); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + }); + SQLExecutor.getInstance().execute(connection, procedureBody, resultSet -> {}); + } catch (Exception e) { + connection.rollback(); + throw new RuntimeException(e); + } finally { + connection.setAutoCommit(true); + } + + } + + @Override + public void connectDatabase(Connection connection, String database) { + if (StringUtils.isEmpty(database)) { + return; + } + try { + SQLExecutor.getInstance().execute(connection, "use " + database + ";"); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + + @Override + public void dropTable(Connection connection, String databaseName, String schemaName, String tableName) { + String sql = "DROP TABLE " + format(tableName); + SQLExecutor.getInstance().execute(connection, sql, resultSet -> null); + } + + @Override + public void deleteProcedure(Connection connection, String databaseName, String schemaName, Procedure procedure) { + String procedureNewName = getSchemaOrProcedureName(procedure.getProcedureBody(), databaseName, procedure); + String sql = "DROP PROCEDURE " + procedureNewName; + SQLExecutor.getInstance().execute(connection, sql, resultSet -> null); + } + + @Override + public void deleteFunction(Connection connection, String databaseName, String schemaName, Function function) { + String functionNewName = getSchemaOrFunctionName(function.getFunctionBody(), databaseName, function); + String sql = "DROP FUNCTION " + functionNewName; + SQLExecutor.getInstance().execute(connection, sql, resultSet -> null); + } + + private static String getSchemaOrProcedureName(String procedureBody, String schemaName, Procedure procedure) { + if (procedureBody.toLowerCase().contains(schemaName.toLowerCase())) { + return procedure.getProcedureName(); + } else { + return schemaName + "." + procedure.getProcedureName(); + } + } + + private static String getSchemaOrFunctionName(String functionBody, String schemaName, Function function) { + if (functionBody.toLowerCase().contains(schemaName.toLowerCase())) { + return function.getFunctionName(); + } else { + return schemaName + "." + function.getFunctionName(); + } + } + + public static String format(String tableName) { + return "`" + tableName + "`"; + } +} diff --git a/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/DuckDBMetaData.java b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/DuckDBMetaData.java new file mode 100644 index 000000000..50ef4bd34 --- /dev/null +++ b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/DuckDBMetaData.java @@ -0,0 +1,255 @@ +package ai.chat2db.plugin.duckdb; + +import ai.chat2db.plugin.duckdb.builder.DuckDBSqlBuilder; +import ai.chat2db.plugin.duckdb.type.DuckDBColumnTypeEnum; +import ai.chat2db.plugin.duckdb.type.DuckDBDefaultValueEnum; +import ai.chat2db.plugin.duckdb.type.DuckDBIndexTypeEnum; +import ai.chat2db.spi.CommandExecutor; +import ai.chat2db.spi.MetaData; +import ai.chat2db.spi.SqlBuilder; +import ai.chat2db.spi.jdbc.DefaultMetaService; +import ai.chat2db.spi.model.*; +import ai.chat2db.spi.sql.SQLExecutor; +import jakarta.validation.constraints.NotEmpty; +import org.apache.commons.lang3.StringUtils; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.stream.Collectors; + +import static ai.chat2db.spi.util.SortUtils.sortDatabase; + +public class DuckDBMetaData extends DefaultMetaService implements MetaData { + + private List systemDatabases = Arrays.asList("information_schema", "temp", "main", "system"); + + @Override + public List databases(Connection connection) { + List databases = SQLExecutor.getInstance().databases(connection); + return sortDatabase(databases, systemDatabases, connection); + } + + @Override + public CommandExecutor getCommandExecutor() { + return new DuckDBCommandExecutor(); + } + + + private static String TABLES_SQL + = "SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_CATALOG = '%s' AND TABLE_SCHEMA = '%s'"; + @Override + public List tables(Connection connection, @NotEmpty String databaseName, String schemaName, String tableName) { + String sql = String.format(TABLES_SQL, databaseName, schemaName); + if(StringUtils.isNotBlank(tableName)){ + sql += " AND TABLE_NAME = '" + tableName + "'"; + } + return SQLExecutor.getInstance().execute(connection, sql, resultSet -> { + List
tables = new ArrayList<>(); + while (resultSet.next()) { + Table table = new Table(); + table.setDatabaseName(databaseName); + table.setSchemaName(schemaName); + table.setName(resultSet.getString("table_name")); + //table.setEngine(resultSet.getString("ENGINE")); + //table.setRows(resultSet.getLong("TABLE_ROWS")); + //table.setDataLength(resultSet.getLong("DATA_LENGTH")); + //table.setCreateTime(resultSet.getString("CREATE_TIME")); + //table.setUpdateTime(resultSet.getString("UPDATE_TIME")); + //table.setCollate(resultSet.getString("TABLE_COLLATION")); + table.setComment(resultSet.getString("TABLE_COMMENT")); + tables.add(table); + } + return tables; + }); + } + + + @Override + public String tableDDL(Connection connection, @NotEmpty String databaseName, String schemaName, + @NotEmpty String tableName) { + String sql = "SELECT sql FROM duckdb_tables() WHERE database_name = " + format(databaseName) + + " AND schema_name = " + format(schemaName) + " AND table_name = " + format(tableName); + return SQLExecutor.getInstance().execute(connection, sql, resultSet -> { + if (resultSet.next()) { + return resultSet.getString("sql"); + } + return null; + }); + } + + public static String format(String tableName) { + return "'" + tableName + "'"; + } + + private static String SELECT_TABLE_COLUMNS = "SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s' order by ORDINAL_POSITION"; + + @Override + public List columns(Connection connection, String databaseName, String schemaName, String tableName) { + String sql = String.format(SELECT_TABLE_COLUMNS, schemaName, tableName); + List tableColumns = new ArrayList<>(); + return SQLExecutor.getInstance().execute(connection, sql, resultSet -> { + while (resultSet.next()) { + TableColumn column = new TableColumn(); + column.setDatabaseName(databaseName); + column.setTableName(tableName); + column.setOldName(resultSet.getString("column_name")); + column.setName(resultSet.getString("column_name")); + //column.setColumnType(resultSet.getString("COLUMN_TYPE")); + column.setColumnType(resultSet.getString("data_type").toUpperCase()); + //column.setDataType(resultSet.getInt("DATA_TYPE")); + column.setDefaultValue(resultSet.getString("column_default")); + //column.setAutoIncrement(resultSet.getString("EXTRA").contains("auto_increment")); + column.setComment(resultSet.getString("COLUMN_COMMENT")); + //column.setPrimaryKey("PRI".equalsIgnoreCase(resultSet.getString("COLUMN_KEY"))); + column.setNullable("YES".equalsIgnoreCase(resultSet.getString("is_nullable")) ? 1 : 0); + column.setOrdinalPosition(resultSet.getInt("ordinal_position")); + column.setDecimalDigits(resultSet.getInt("numeric_precision")); + column.setCharSetName(resultSet.getString("character_set_name")); + column.setCollationName(resultSet.getString("collation_name")); + setColumnSize(column, resultSet.getString("data_type")); + tableColumns.add(column); + } + return tableColumns; + }); + } + + private void setColumnSize(TableColumn column, String columnType) { + try { + if (columnType.contains("(")) { + String size = columnType.substring(columnType.indexOf("(") + 1, columnType.indexOf(")")); + if ("SET".equalsIgnoreCase(column.getColumnType()) || "ENUM".equalsIgnoreCase(column.getColumnType())) { + column.setValue(size); + } else { + if (size.contains(",")) { + String[] sizes = size.split(","); + if (StringUtils.isNotBlank(sizes[0])) { + column.setColumnSize(Integer.parseInt(sizes[0])); + } + if (StringUtils.isNotBlank(sizes[1])) { + column.setDecimalDigits(Integer.parseInt(sizes[1])); + } + } else { + column.setColumnSize(Integer.parseInt(size)); + } + } + } + } catch (Exception e) { + } + } + + private static String VIEW_DDL_SQL = "SELECT sql FROM duckdb_views() WHERE database_name = '%s' AND schema_name = '%s' AND view_name = '%s'"; + + @Override + public Table view(Connection connection, String databaseName, String schemaName, String viewName) { + String sql = String.format(VIEW_DDL_SQL, databaseName, schemaName, viewName); + return SQLExecutor.getInstance().execute(connection, sql, resultSet -> { + Table table = new Table(); + table.setDatabaseName(databaseName); + table.setSchemaName(schemaName); + table.setName(viewName); + if (resultSet.next()) { + table.setDdl(resultSet.getString("sql")); + } + return table; + }); + } + + + @Override + public List indexes(Connection connection, String databaseName, String schemaName, String tableName) { + StringBuilder queryBuf = new StringBuilder("SELECT * FROM duckdb_indexes WHERE schema_name = "); + queryBuf.append("'").append(schemaName).append("'"); + queryBuf.append(" and table_name = "); + queryBuf.append("'").append(tableName).append("'"); + return SQLExecutor.getInstance().execute(connection, queryBuf.toString(), resultSet -> { + LinkedHashMap map = new LinkedHashMap(); + while (resultSet.next()) { + String keyName = resultSet.getString("Key_name"); + TableIndex tableIndex = map.get(keyName); + if (tableIndex != null) { + List columnList = tableIndex.getColumnList(); + columnList.add(getTableIndexColumn(resultSet)); + columnList = columnList.stream().sorted(Comparator.comparing(TableIndexColumn::getOrdinalPosition)) + .collect(Collectors.toList()); + tableIndex.setColumnList(columnList); + } else { + TableIndex index = new TableIndex(); + index.setDatabaseName(databaseName); + index.setSchemaName(schemaName); + index.setTableName(tableName); + index.setName(keyName); + index.setUnique(!resultSet.getBoolean("Non_unique")); + index.setType(resultSet.getString("Index_type")); + index.setComment(resultSet.getString("Index_comment")); + List tableIndexColumns = new ArrayList<>(); + tableIndexColumns.add(getTableIndexColumn(resultSet)); + index.setColumnList(tableIndexColumns); + if ("PRIMARY".equalsIgnoreCase(keyName)) { + index.setType(DuckDBIndexTypeEnum.PRIMARY_KEY.getName()); + } else if (index.getUnique()) { + index.setType(DuckDBIndexTypeEnum.UNIQUE.getName()); + } else { + index.setType(DuckDBIndexTypeEnum.NORMAL.getName()); + } + map.put(keyName, index); + } + } + return map.values().stream().collect(Collectors.toList()); + }); + + } + + private TableIndexColumn getTableIndexColumn(ResultSet resultSet) throws SQLException { + TableIndexColumn tableIndexColumn = new TableIndexColumn(); + tableIndexColumn.setColumnName(resultSet.getString("Column_name")); + tableIndexColumn.setOrdinalPosition(resultSet.getShort("Seq_in_index")); + tableIndexColumn.setCollation(resultSet.getString("Collation")); + tableIndexColumn.setCardinality(resultSet.getLong("Cardinality")); + tableIndexColumn.setSubPart(resultSet.getLong("Sub_part")); + String collation = resultSet.getString("Collation"); + if ("a".equalsIgnoreCase(collation)) { + tableIndexColumn.setAscOrDesc("ASC"); + } else if ("d".equalsIgnoreCase(collation)) { + tableIndexColumn.setAscOrDesc("DESC"); + } + return tableIndexColumn; + } + + @Override + public SqlBuilder getSqlBuilder() { + return new DuckDBSqlBuilder(); + } + + @Override + public TableMeta getTableMeta(String databaseName, String schemaName, String tableName) { + return TableMeta.builder() + .columnTypes(DuckDBColumnTypeEnum.getTypes()) + //.collations(MysqlCollationEnum.getCollations()) + .indexTypes(DuckDBIndexTypeEnum.getIndexTypes()) + .defaultValues(DuckDBDefaultValueEnum.getDefaultValues()) + .build(); + } + + @Override + public String getMetaDataName(String... names) { + return Arrays.stream(names).filter(name -> StringUtils.isNotBlank(name)).collect(Collectors.joining(".")); + } + +// @Override +// public ValueHandler getValueHandler() { +// return new MysqlValueHandler(); +// } + + /*@Override + public ValueProcessor getValueProcessor() { + return new DuckDBValueProcessor(); + }*/ + + @Override + public List getSystemDatabases() { + return systemDatabases; + } + +} diff --git a/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/DuckDBPlugin.java b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/DuckDBPlugin.java new file mode 100644 index 000000000..648ecd3aa --- /dev/null +++ b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/DuckDBPlugin.java @@ -0,0 +1,25 @@ +package ai.chat2db.plugin.duckdb; + +import ai.chat2db.spi.DBManage; +import ai.chat2db.spi.MetaData; +import ai.chat2db.spi.Plugin; +import ai.chat2db.spi.config.DBConfig; +import ai.chat2db.spi.util.FileUtils; + +public class DuckDBPlugin implements Plugin { + + @Override + public DBConfig getDBConfig() { + return FileUtils.readJsonValue(this.getClass(),"duckDB.json", DBConfig.class); + } + + @Override + public MetaData getMetaData() { + return new DuckDBMetaData(); + } + + @Override + public DBManage getDBManage() { + return new DuckDBManage(); + } +} diff --git a/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/builder/DuckDBSqlBuilder.java b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/builder/DuckDBSqlBuilder.java new file mode 100644 index 000000000..6a981ad6c --- /dev/null +++ b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/builder/DuckDBSqlBuilder.java @@ -0,0 +1,175 @@ +package ai.chat2db.plugin.duckdb.builder; + +import ai.chat2db.plugin.duckdb.type.DuckDBColumnTypeEnum; +import ai.chat2db.plugin.duckdb.type.DuckDBIndexTypeEnum; +import ai.chat2db.spi.enums.EditStatus; +import ai.chat2db.spi.jdbc.DefaultSqlBuilder; +import ai.chat2db.spi.model.Database; +import ai.chat2db.spi.model.Table; +import ai.chat2db.spi.model.TableColumn; +import ai.chat2db.spi.model.TableIndex; +import ai.chat2db.spi.util.SqlUtils; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + + +public class DuckDBSqlBuilder extends DefaultSqlBuilder { + @Override + public String buildCreateTableSql(Table table) { + StringBuilder script = new StringBuilder(); + script.append("CREATE TABLE "); + if (StringUtils.isNotBlank(table.getSchemaName())) { + script.append(table.getSchemaName()).append("."); + } + script.append(table.getName()).append(" (").append("\n"); + + // append column + for (TableColumn column : table.getColumnList()) { + if (StringUtils.isBlank(column.getName()) || StringUtils.isBlank(column.getColumnType())) { + continue; + } + DuckDBColumnTypeEnum typeEnum = DuckDBColumnTypeEnum.getByType(column.getColumnType()); + if (typeEnum == null) { + continue; + } + script.append("\t").append(typeEnum.buildCreateColumnSql(column)).append(",\n"); + } + + // append primary key and index + for (TableIndex tableIndex : table.getIndexList()) { + if (StringUtils.isBlank(tableIndex.getName()) || StringUtils.isBlank(tableIndex.getType())) { + continue; + } + DuckDBIndexTypeEnum mysqlIndexTypeEnum = DuckDBIndexTypeEnum.getByType(tableIndex.getType()); + if (mysqlIndexTypeEnum == null) { + continue; + } + script.append("\t").append(mysqlIndexTypeEnum.buildCreateIndexScript(tableIndex)).append(",\n"); + } + + script = new StringBuilder(script.substring(0, script.length() - 2)); + script.append("\n);\n"); + + + if (StringUtils.isNotBlank(table.getComment())) { + script.append(" COMMENT ON TABLE ").append(table.getSchemaName()).append(".").append(table.getName()) + .append(" IS '").append(table.getComment()).append("'"); + } + + script.append(";"); + + return script.toString(); + } + + @Override + public String buildModifyTaleSql(Table oldTable, Table newTable) { + StringBuilder tableBuilder = new StringBuilder(); + + if (!StringUtils.equalsIgnoreCase(oldTable.getName(), newTable.getName())) { + tableBuilder.append("ALTER TABLE ").append(oldTable.getSchemaName()).append(".").append(oldTable.getName()) + .append(" RENAME TO ").append("'").append(newTable.getName()).append("'").append(";\n"); + } + + if (!StringUtils.equalsIgnoreCase(oldTable.getComment(), newTable.getComment())) { + tableBuilder.append("COMMENT ON TABLE ").append(oldTable.getSchemaName()).append(".").append(oldTable.getName()) + .append(" IS ").append("'").append(newTable.getComment()).append("'").append(";\n"); + } + + + // append modify column + for (TableColumn tableColumn : newTable.getColumnList()) { + if ((StringUtils.isNotBlank(tableColumn.getEditStatus()) && StringUtils.isNotBlank(tableColumn.getColumnType()) + && StringUtils.isNotBlank(tableColumn.getName()))) { + DuckDBColumnTypeEnum typeEnum = DuckDBColumnTypeEnum.getByType(tableColumn.getColumnType()); + if (typeEnum == null) { + continue; + } + tableBuilder.append("\t").append(typeEnum.buildModifyColumn(tableColumn)).append("\n"); + + } + } + + // append modify index + for (TableIndex tableIndex : newTable.getIndexList()) { + if (StringUtils.isNotBlank(tableIndex.getEditStatus()) && StringUtils.isNotBlank(tableIndex.getType())) { + DuckDBIndexTypeEnum duckDBIndexTypeEnum = DuckDBIndexTypeEnum.getByType(tableIndex.getType()); + if (duckDBIndexTypeEnum == null) { + continue; + } + tableBuilder.append("\t").append(duckDBIndexTypeEnum.buildModifyIndex(tableIndex)).append(";\n"); + } + } + + // append reorder column + // script.append(buildGenerateReorderColumnSql(oldTable, newTable)); + + if (tableBuilder.length() > 2) { + tableBuilder = new StringBuilder(tableBuilder.substring(0, tableBuilder.length() - 2)); + tableBuilder.append(";"); + return tableBuilder.toString(); + } else { + return StringUtils.EMPTY; + } + + } + + @Override + public String pageLimit(String sql, int offset, int pageNo, int pageSize) { + StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14); + sqlBuilder.append(sql); + if (offset == 0) { + sqlBuilder.append("\n LIMIT "); + sqlBuilder.append(pageSize); + } else { + sqlBuilder.append("\n LIMIT "); + sqlBuilder.append(offset); + sqlBuilder.append(","); + sqlBuilder.append(pageSize); + } + return sqlBuilder.toString(); + } + + + @Override + public String buildCreateDatabaseSql(Database database) { + StringBuilder sqlBuilder = new StringBuilder(); + sqlBuilder.append("CREATE DATABASE " + database.getName()); + if (StringUtils.isNotBlank(database.getCharset())) { + sqlBuilder.append(" DEFAULT CHARACTER SET=").append(database.getCharset()); + } + if (StringUtils.isNotBlank(database.getCollation())) { + sqlBuilder.append(" COLLATE=").append(database.getCollation()); + } + return sqlBuilder.toString(); + } + + + @Override + protected void buildTableName(String databaseName, String schemaName, String tableName, StringBuilder script) { + if (StringUtils.isNotBlank(databaseName)) { + script.append(SqlUtils.quoteObjectName(databaseName, "'")).append('.'); + } + if (StringUtils.isNotBlank(schemaName)) { + script.append(SqlUtils.quoteObjectName(schemaName, "'")).append('.'); + } + script.append(SqlUtils.quoteObjectName(tableName, "'")); + } + + /** + * @param columnList + * @param script + */ + @Override + protected void buildColumns(List columnList, StringBuilder script) { + if (CollectionUtils.isNotEmpty(columnList)) { + script.append(" (") + .append(columnList.stream().map(s -> SqlUtils.quoteObjectName(s, "`")).collect(Collectors.joining(","))) + .append(") "); + } + } + +} diff --git a/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/builder/form.json b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/builder/form.json new file mode 100644 index 000000000..e24020fbe --- /dev/null +++ b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/builder/form.json @@ -0,0 +1,45 @@ +{ + "baseInfo": { + "items": [ + { + "defaultValue": "@localhost", + "inputType": "INPUT", + "labelNameCN": "名称", + "labelNameEN": "Name", + "name": "alias", + "required": true, + "width": 100, + }, + { + "defaultValue": "localhost", + "inputType": "INPUT", + "labelNameCN": "主机", + "labelNameEN": "Host", + "name": "host", + "required": true, + "width": 70, + }, + { + "defaultValue": "", + "inputType": "INPUT", + "labelNameCN": "数据库", + "labelNameEN": "Database", + "name": "database", + "required": false, + "width": 100 + }, + { + "defaultValue": "jdbc:duckdb:{file}", + "inputType": "INPUT", + "labelNameCN": "URL", + "labelNameEN": "URL", + "name": "url", + "required": true, + "width": 100 + } + ], + "pattern": "/jdbc:duckdb:\/\/(\\w+)", + "template": "jdbc:duckdb://{host}" + }, + "type":"DuckDB" +} \ No newline at end of file diff --git a/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/duckDB.json b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/duckDB.json new file mode 100644 index 000000000..0c4dec309 --- /dev/null +++ b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/duckDB.json @@ -0,0 +1,18 @@ +{ + "dbType": "DUCKDB", + "supportDatabase": true, + "supportSchema": true, + "driverConfigList": [ + { + "url": "jdbc:duckdb://", + "defaultDriver": true, + "custom": false, + "downloadJdbcDriverUrls": [ + "https://cdn.chat2db-ai.com/lib/duckdb_jdbc-1.1.3.jar" + ], + "jdbcDriver": "duckdb_jdbc-1.1.3.jar", + "jdbcDriverClass": "org.duckdb.DuckDBDriver" + } + ], + "name": "DuckDB" +} diff --git a/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/type/DuckDBColumnTypeEnum.java b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/type/DuckDBColumnTypeEnum.java new file mode 100644 index 000000000..9b4374284 --- /dev/null +++ b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/type/DuckDBColumnTypeEnum.java @@ -0,0 +1,413 @@ +package ai.chat2db.plugin.duckdb.type; + +import ai.chat2db.spi.ColumnBuilder; +import ai.chat2db.spi.enums.EditStatus; +import ai.chat2db.spi.model.ColumnType; +import ai.chat2db.spi.model.TableColumn; +import ai.chat2db.spi.util.SqlUtils; +import com.google.common.collect.Maps; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public enum DuckDBColumnTypeEnum implements ColumnBuilder { + + JSON("JSON", false, false, true, false, false, false, true, true, false, false), + + BIGINT("BIGINT", false, false, true, false, false, false, true, true, false, false), + + BINARY("BINARY", false, false, true, false, false, false, true, true, false, false), + + BIT("BIT", false, false, true, false, false, false, true, true, false, false), + + BLOB("BLOB", false, false, true, false, false, false, true, true, false, false), + + BOOL("BOOL", false, false, true, false, false, false, true, true, false, false), + + BOOLEAN("BOOLEAN", false, false, true, false, false, false, true, true, false, false), + + BPCHAR("BPCHAR", false, false, true, false, false, false, true, true, false, false), + + BYTEA("BYTEA", false, false, true, false, false, false, true, true, false, false), + + CHAR("CHAR", true, false, true, false, false, false, true, true, false, false), + + DATE("DATE", false, false, true, false, false, false, true, true, false, false), + + DATETIME("DATETIME", false, false, true, false, false, false, true, true, false, false), + + DEC("DEC", false, false, true, false, false, false, true, true, false, false), + + DECIMAL("DECIMAL", true, true, true, false, false, false, true, true, false, false), + + DOUBLE("DOUBLE", false, false, true, false, false, false, true, true, false, false), + + ENUM("ENUM", false, false, true, false, false, false, true, true, false, false), + + FLOAT("FLOAT", true, false, true, false, false, false, true, true, false, false), + + FLOAT4("FLOAT4", true, false, true, false, false, false, true, true, false, false), + + FLOAT8("FLOAT8", true, false, true, false, false, false, true, true, false, false), + + GUID("GUID", false, false, true, false, false, false, true, true, false, false), + + HUGEINT("HUGEINT", false, false, true, true, false, false, true, true, false, false), + + INT("INT", false, false, true, false, false, false, true, true, false, false), + INT1("INT1", false, false, true, false, false, false, true, true, false, false), + INT128("INT128", false, false, true, false, false, false, true, true, false, false), + INT16("INT16", false, false, true, false, false, false, true, true, false, false), + INT2("INT2", false, false, true, false, false, false, true, true, false, false), + INT32("INT32", false, false, true, false, false, false, true, true, false, false), + INT4("INT4", false, false, true, false, false, false, true, true, false, false), + INT64("INT64", false, false, true, false, false, false, true, true, false, false), + INT8("INT8", false, false, true, false, false, false, true, true, false, false), + + INTEGER("INTEGER", false, false, true, false, false, false, true, true, false, false), + + INTEGRAL("INTEGRAL", false, false, true, false, false, false, true, true, false, false), + INTERVAL("INTERVAL", false, false, true, false, false, false, true, true, false, false), + + LIST("LIST", false, false, true, false, false, false, true, true, false, false), + + LOGICAL("LOGICAL", false, false, true, false, false, false, true, true, false, false), + + LONG("LONG", false, false, true, false, false, false, true, true, false, false), + + MAP("MAP", false, false, true, false, false, false, true, true, false, false), + + NULL("NULL", false, false, true, false, false, false, true, true, false, false), + + NUMERIC("NUMERIC", false, false, true, false, false, false, true, true, false, false), + + NVARCHAR("NVARCHAR", true, false, true, false, false, false, true, true, false, false), + + OID("OID", false, false, true, false, false, false, true, true, false, false), + + REAL("REAL", false, false, true, false, false, false, true, true, false, false), + + ROW("ROW", false, false, true, false, false, false, true, true, false, false), + + SHORT("SHORT", false, false, true, false, false, false, true, true, false, false), + + SIGNED("SIGNED", false, false, true, false, false, false, true, true, false, false), + + SMALLINT("SMALLINT", false, false, true, false, false, false, true, true, false, false), + + STRING("STRING", false, false, true, false, false, false, true, true, false, false), + + STRUCT("STRUCT", false, false, true, false, false, false, true, true, false, false), + + TEXT("TEXT", false, false, true, false, false, false, true, true, false, false), + + TIME("TIME", false, false, true, false, false, false, true, true, false, false), + + TIMESTAMP("TIMESTAMP", false, false, true, false, false, false, true, true, false, false), + + TIMSTAMP_MS("TIMSTAMP_MS", false, false, true, false, false, false, true, true, false, false), + + TIMSTAMP_NS("TIMSTAMP_NS", false, false, true, false, false, false, true, true, false, false), + TIMSTAMP_S("TIMSTAMP_S", false, false, true, false, false, false, true, true, false, false), + TIMSTAMP_US("TIMSTAMP_US", false, false, true, false, false, false, true, true, false, false), + + TIMESTAMP_WITH_TIME_ZONE("TIMESTAMP WITH TIME ZONE", false, false, true, false, false, false, true, true, false, false), + + TIME_WITH_TIME_ZONE("TIME WITH TIME ZONE", false, false, true, false, false, false, true, true, false, false), + + + TINYINT("TINYINT", false, false, true, false, false, false, true, true, false, false), + + UBIGINT("UBIGINT", false, false, true, false, false, false, true, true, false, false), + + UHUGEINT("UHUGEINT", false, false, true, false, false, false, true, true, false, false), + + UINT128("UINT128", false, false, true, false, false, false, true, true, false, false), + + UINT16("UINT16", false, false, true, false, false, false, true, true, false, false), + + UINT32("UINT32", false, false, true, false, false, false, true, true, false, false), + + UINT64("UINT64", false, false, true, false, false, false, true, true, false, false), + + UINT8("UINT8", false, false, true, false, false, false, true, true, false, false), + + UINTEGER("UINTEGER", false, false, true, false, false, false, true, true, false, false), + + UNION("UNION", false, false, true, false, false, false, true, true, false, false), + + USMALLINT("USMALLINT", false, false, true, false, false, false, true, true, false, false), + + UTINYINT("UTINYINT", false, false, true, false, false, false, true, true, false, false), + + UUID("UUID", false, false, true, false, false, false, true, true, false, false), + + VARBINARY("VARBINARY", false, false, true, false, false, false, true, true, false, false), + + VARCHAR("VARCHAR", true, false, true, false, false, false, true, true, false, true), + + VARINT("VARINT", false, false, true, false, false, false, true, true, false, false), + + ARRAY("ARRAY", false, false, true, false, false, false, true, true, false, false), + ; + private ColumnType columnType; + + public static DuckDBColumnTypeEnum getByType(String dataType) { + String type = SqlUtils.removeDigits(dataType.toUpperCase()); + return COLUMN_TYPE_MAP.get(type); + } + + private static Map COLUMN_TYPE_MAP = Maps.newHashMap(); + + static { + for (DuckDBColumnTypeEnum value : DuckDBColumnTypeEnum.values()) { + COLUMN_TYPE_MAP.put(value.getColumnType().getTypeName(), value); + } + } + + public ColumnType getColumnType() { + return columnType; + } + + + DuckDBColumnTypeEnum(String dataTypeName, boolean supportLength, boolean supportScale, boolean supportNullable, boolean supportAutoIncrement, boolean supportCharset, boolean supportCollation, boolean supportComments, boolean supportDefaultValue, boolean supportExtent, boolean supportUnit) { + this.columnType = new ColumnType(dataTypeName, supportLength, supportScale, supportNullable, supportAutoIncrement, supportCharset, supportCollation, supportComments, supportDefaultValue, supportExtent, false, supportUnit); + } + + @Override + public String buildCreateColumnSql(TableColumn column) { + DuckDBColumnTypeEnum type = COLUMN_TYPE_MAP.get(column.getColumnType().toUpperCase()); + if (type == null) { + return ""; + } + StringBuilder script = new StringBuilder(); + + script.append(column.getName()).append(" "); + + script.append(buildDataType(column, type)).append(" "); + + script.append(buildDefaultValue(column, type)).append(" "); + + script.append(buildAutoIncrement(column,type)).append(" "); + + script.append(buildCreateNullable(column, type)).append(" "); + + return script.toString(); + } + + private String buildAutoIncrement(TableColumn column, DuckDBColumnTypeEnum type) { + if(!type.getColumnType().isSupportAutoIncrement()){ + return ""; + } + if (column.getAutoIncrement() != null && column.getAutoIncrement() + && column.getSeed() != null && column.getSeed() > 0 && column.getIncrement() != null && column.getIncrement() > 0) { + return "IDENTITY(" + column.getSeed() + "," + column.getIncrement() + ")"; + } + if (column.getAutoIncrement() != null && column.getAutoIncrement()) { + return "IDENTITY(1,1)"; + } + return ""; + } + + private String buildNullable(TableColumn column, DuckDBColumnTypeEnum type) { + if (!type.getColumnType().isSupportNullable()) { + return ""; + } + if (column.getNullable() != null && 1 == column.getNullable()) { + return "DROP NOT NULL"; + } else { + return "SET NOT NULL"; + } + } + + private String buildCreateNullable(TableColumn column, DuckDBColumnTypeEnum type) { + if (!type.getColumnType().isSupportNullable()) { + return ""; + } + if (column.getNullable() != null && 1 == column.getNullable()) { + return ""; + } else { + return "NOT NULL"; + } + } + + private String buildDefaultValue(TableColumn column, DuckDBColumnTypeEnum type) { + if (!type.getColumnType().isSupportDefaultValue() || StringUtils.isEmpty(column.getDefaultValue())) { + return ""; + } + + StringBuilder script = new StringBuilder(); + script.append("ALTER TABLE ").append(column.getSchemaName()).append(".").append(column.getTableName()); + script.append(column.getOldName()).append(" "); + if ("EMPTY_STRING".equalsIgnoreCase(column.getDefaultValue().trim())) { + return script.append("SET DEFAULT '';\n").toString(); + } + + if ("NULL".equalsIgnoreCase(column.getDefaultValue().trim())) { + return script.append("SET DEFAULT NULL;\n").toString(); + } + + return script.append("SET DEFAULT ").append(column.getDefaultValue()).append(";\n").toString(); + } + + private String buildDataType(TableColumn column, DuckDBColumnTypeEnum type) { + String columnType = type.columnType.getTypeName(); + if (Arrays.asList(VARCHAR, STRING, BPCHAR, NVARCHAR, TEXT).contains(type)) { + StringBuilder script = new StringBuilder(); + script.append(columnType); + if (column.getColumnSize() != null && StringUtils.isEmpty(column.getUnit())) { + script.append("(").append(column.getColumnSize()).append(")"); + } else if (column.getColumnSize() != null && !StringUtils.isEmpty(column.getUnit())) { + script.append("(").append(column.getColumnSize()).append(" ").append(column.getUnit()).append(")"); + } + return script.toString(); + } + + if (Arrays.asList(DECIMAL, FLOAT, TIMESTAMP).contains(type)) { + StringBuilder script = new StringBuilder(); + script.append(columnType); + if (column.getColumnSize() != null && column.getDecimalDigits() == null) { + script.append("(").append(column.getColumnSize()).append(")"); + } else if (column.getColumnSize() != null && column.getDecimalDigits() != null) { + script.append("(").append(column.getColumnSize()).append(",").append(column.getDecimalDigits()).append(")"); + } + return script.toString(); + } + + if (Arrays.asList(TIME_WITH_TIME_ZONE, TIMSTAMP_US).contains(type)) { + StringBuilder script = new StringBuilder(); + if (column.getColumnSize() == null) { + script.append(columnType); + } else { + String[] split = columnType.split("TIMESTAMP"); + script.append("TIMESTAMP").append("(").append(column.getColumnSize()).append(")").append(split[1]); + } + return script.toString(); + } + return columnType; + } + + private String buildModifyDataType(TableColumn column, DuckDBColumnTypeEnum type) { + String columnType = type.columnType.getTypeName(); + if (Arrays.asList(VARCHAR, STRING, BPCHAR, NVARCHAR, TEXT).contains(type)) { + StringBuilder script = new StringBuilder(); + script.append(columnType); + if (column.getColumnSize() != null && StringUtils.isEmpty(column.getUnit())) { + script.append("(").append(column.getColumnSize()).append(")"); + } else if (column.getColumnSize() != null && !StringUtils.isEmpty(column.getUnit())) { + script.append("(").append(column.getColumnSize()).append(" ").append(column.getUnit()).append(")"); + } + return script.toString(); + } + + if (Arrays.asList(DECIMAL, FLOAT, TIMESTAMP).contains(type)) { + StringBuilder script = new StringBuilder(); + script.append(columnType); + if (column.getColumnSize() != null && column.getDecimalDigits() == null) { + script.append("(").append(column.getColumnSize()).append(")"); + } else if (column.getColumnSize() != null && column.getDecimalDigits() != null) { + script.append("(").append(column.getColumnSize()).append(",").append(column.getDecimalDigits()).append(")"); + } + return script.toString(); + } + + if (Arrays.asList(TIME_WITH_TIME_ZONE, TIMSTAMP_US).contains(type)) { + StringBuilder script = new StringBuilder(); + if (column.getColumnSize() == null) { + script.append(columnType); + } else { + String[] split = columnType.split("TIMESTAMP"); + script.append("TIMESTAMP").append("(").append(column.getColumnSize()).append(")").append(split[1]); + } + return script.toString(); + } + return columnType; + } + + + + @Override + public String buildModifyColumn(TableColumn tableColumn) { + + if (EditStatus.DELETE.name().equals(tableColumn.getEditStatus())) { + StringBuilder script = new StringBuilder(); + script.append("ALTER TABLE ").append(tableColumn.getSchemaName()).append(".").append(tableColumn.getTableName()); + script.append(" ").append("DROP ").append(tableColumn.getName()).append(";\n"); + return script.toString(); + } + if (EditStatus.ADD.name().equals(tableColumn.getEditStatus())) { + StringBuilder script = new StringBuilder(); + script.append(buildModifyADDColumnSql(tableColumn)).append(";\n"); + return script.toString(); + } + if (EditStatus.MODIFY.name().equals(tableColumn.getEditStatus())) { + StringBuilder script = new StringBuilder(); + + script.append(buildModifyColumnSql(tableColumn, tableColumn.getOldColumn())).append(" \n"); + + if (!StringUtils.equalsIgnoreCase(tableColumn.getOldName(), tableColumn.getName())) { + script.append("ALTER TABLE ").append(tableColumn.getSchemaName()).append(".").append(tableColumn.getTableName()); + script.append(" ").append("RENAME ").append(tableColumn.getOldName()).append(" TO ").append(tableColumn.getName()).append(";\n"); + } + return script.toString(); + + } + return ""; + } + + public String buildModifyColumnSql(TableColumn column, TableColumn oldColumn) { + DuckDBColumnTypeEnum type = COLUMN_TYPE_MAP.get(column.getColumnType().toUpperCase()); + if (type == null) { + return ""; + } + StringBuilder script = new StringBuilder(); + + if (!column.getColumnType().equals(oldColumn.getColumnType())) { + script.append("ALTER TABLE ").append(column.getSchemaName()).append(".").append(column.getTableName()).append(" "); + script.append("ALTER ").append(oldColumn.getName()).append(" SET DATA TYPE ").append(buildModifyDataType(column, type)).append(";\n"); + } + + script.append(buildDefaultValue(column, type)).append(" "); + + if (oldColumn.getNullable() != column.getNullable()) { + script.append("ALTER TABLE ").append(column.getSchemaName()).append(".").append(column.getTableName()).append(" "); + script.append("ALTER COLUMN ").append(column.getName()).append(" ").append(buildNullable(column, type)).append(";\n"); + } + + return script.toString(); + } + + public String buildModifyADDColumnSql(TableColumn column) { + DuckDBColumnTypeEnum type = COLUMN_TYPE_MAP.get(column.getColumnType().toUpperCase()); + if (type == null) { + return ""; + } + StringBuilder script = new StringBuilder(); + script.append("ALTER TABLE ").append(column.getSchemaName()).append(".").append(column.getTableName()); + script.append(" ").append("ADD COLUMN ").append(column.getName()).append(" ").append(buildModifyDataType(column, type)); + if (!type.getColumnType().isSupportDefaultValue() || StringUtils.isEmpty(column.getDefaultValue())) { + return script.append(";\n").toString(); + } else { + if ("EMPTY_STRING".equalsIgnoreCase(column.getDefaultValue().trim())) { + return script.append(" ").append("DEFAULT '';\n").toString(); + } + + if ("NULL".equalsIgnoreCase(column.getDefaultValue().trim())) { + return script.append(" ").append("DEFAULT NULL;\n").toString(); + } + script.append(" ").append("DEFAULT ").append(column.getDefaultValue()).append(";\n"); + } + + + + return script.toString(); + } + + public static List getTypes() { + return Arrays.stream(DuckDBColumnTypeEnum.values()).map(columnTypeEnum -> + columnTypeEnum.getColumnType() + ).toList(); + } +} diff --git a/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/type/DuckDBDefaultValueEnum.java b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/type/DuckDBDefaultValueEnum.java new file mode 100644 index 000000000..ee1152776 --- /dev/null +++ b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/type/DuckDBDefaultValueEnum.java @@ -0,0 +1,27 @@ +package ai.chat2db.plugin.duckdb.type; + +import ai.chat2db.spi.model.DefaultValue; + +import java.util.Arrays; +import java.util.List; + +public enum DuckDBDefaultValueEnum { + EMPTY_STRING("EMPTY_STRING"), + NULL("NULL"), + ; + private DefaultValue defaultValue; + + DuckDBDefaultValueEnum(String defaultValue) { + this.defaultValue = new DefaultValue(defaultValue); + } + + + public DefaultValue getDefaultValue() { + return defaultValue; + } + + public static List getDefaultValues() { + return Arrays.stream(DuckDBDefaultValueEnum.values()).map(DuckDBDefaultValueEnum::getDefaultValue).collect(java.util.stream.Collectors.toList()); + } + +} diff --git a/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/type/DuckDBIndexTypeEnum.java b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/type/DuckDBIndexTypeEnum.java new file mode 100644 index 000000000..eaea01003 --- /dev/null +++ b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/java/ai/chat2db/plugin/duckdb/type/DuckDBIndexTypeEnum.java @@ -0,0 +1,143 @@ +package ai.chat2db.plugin.duckdb.type; + +import ai.chat2db.spi.enums.EditStatus; +import ai.chat2db.spi.model.IndexType; +import ai.chat2db.spi.model.TableIndex; +import ai.chat2db.spi.model.TableIndexColumn; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.List; + +public enum DuckDBIndexTypeEnum { + + PRIMARY_KEY("Primary", "PRIMARY KEY"), + + NORMAL("Normal", "INDEX"), + + UNIQUE("Unique", "UNIQUE INDEX"), + + BITMAP("BITMAP", "BITMAP INDEX"); + + + + public IndexType getIndexType() { + return indexType; + } + + public void setIndexType(IndexType indexType) { + this.indexType = indexType; + } + + private IndexType indexType; + + + public String getName() { + return name; + } + + private String name; + + + public String getKeyword() { + return keyword; + } + + private String keyword; + + DuckDBIndexTypeEnum(String name, String keyword) { + this.name = name; + this.keyword = keyword; + this.indexType = new IndexType(name); + } + + + public static DuckDBIndexTypeEnum getByType(String type) { + for (DuckDBIndexTypeEnum value : DuckDBIndexTypeEnum.values()) { + if (value.name.equalsIgnoreCase(type)) { + return value; + } + } + return null; + } + + public String buildIndexScript(TableIndex tableIndex) { + StringBuilder script = new StringBuilder(); + if (PRIMARY_KEY.equals(this)) { + script.append("ALTER TABLE ").append(tableIndex.getSchemaName()).append(".").append(tableIndex.getTableName()).append(" ADD PRIMARY KEY ").append(buildIndexColumn(tableIndex)); + } else { + if (UNIQUE.equals(this)) { + script.append("CREATE UNIQUE INDEX "); + } else { + script.append("CREATE INDEX "); + } + script.append(buildIndexName(tableIndex)).append(" ON \"").append(tableIndex.getSchemaName()).append("\".\"").append(tableIndex.getTableName()).append("\" ").append(buildIndexColumn(tableIndex)); + } + return script.toString(); + } + + public String buildCreateIndexScript(TableIndex tableIndex) { + StringBuilder script = new StringBuilder(); + if (PRIMARY_KEY.equals(this)) { + script.append("CONSTRAINT ").append(tableIndex.getTableName()).append("_").append("PK PRIMARY KEY ").append(buildIndexColumn(tableIndex)); + } else { + if (UNIQUE.equals(this)) { + script.append("CREATE UNIQUE INDEX "); + } else { + script.append("CREATE INDEX "); + } + script.append(buildIndexName(tableIndex)).append(" ON \"").append(tableIndex.getSchemaName()).append("\".\"").append(tableIndex.getTableName()).append("\" ").append(buildIndexColumn(tableIndex)); + } + return script.toString(); + } + + private String buildIndexColumn(TableIndex tableIndex) { + StringBuilder script = new StringBuilder(); + script.append("("); + for (TableIndexColumn column : tableIndex.getColumnList()) { + if (StringUtils.isNotBlank(column.getColumnName())) { + script.append(column.getColumnName()); + if (!StringUtils.isBlank(column.getAscOrDesc()) && !PRIMARY_KEY.equals(this)) { + script.append(" ").append(column.getAscOrDesc()); + } + script.append(","); + } + } + script.deleteCharAt(script.length() - 1); + script.append(")"); + return script.toString(); + } + + private String buildIndexName(TableIndex tableIndex) { + return "\"" + tableIndex.getSchemaName() + "\"." + "\"" + tableIndex.getName() + "\""; + } + + public String buildModifyIndex(TableIndex tableIndex) { + if (EditStatus.DELETE.name().equals(tableIndex.getEditStatus())) { + return buildDropIndex(tableIndex); + } + if (EditStatus.MODIFY.name().equals(tableIndex.getEditStatus())) { + return StringUtils.join(buildDropIndex(tableIndex), ";\n", buildIndexScript(tableIndex)); + } + if (EditStatus.ADD.name().equals(tableIndex.getEditStatus())) { + return StringUtils.join(buildIndexScript(tableIndex)); + } + return ""; + } + + private String buildDropIndex(TableIndex tableIndex) { + if (DuckDBIndexTypeEnum.PRIMARY_KEY.getName().equals(tableIndex.getType())) { + String tableName = "" + tableIndex.getSchemaName() + "." + tableIndex.getTableName() ; + return StringUtils.join("ALTER TABLE ",tableName," DROP PRIMARY KEY"); + } + StringBuilder script = new StringBuilder(); + script.append("DROP INDEX "); + script.append(buildIndexName(tableIndex)); + + return script.toString(); + } + + public static List getIndexTypes() { + return Arrays.asList(DuckDBIndexTypeEnum.values()).stream().map(DuckDBIndexTypeEnum::getIndexType).collect(java.util.stream.Collectors.toList()); + } +} diff --git a/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/resources/META-INF/services/ai.chat2db.spi.Plugin b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/resources/META-INF/services/ai.chat2db.spi.Plugin new file mode 100644 index 000000000..46d0d0d83 --- /dev/null +++ b/chat2db-server/chat2db-plugins/chat2db-duckdb/src/main/resources/META-INF/services/ai.chat2db.spi.Plugin @@ -0,0 +1 @@ +ai.chat2db.plugin.duckdb.DuckDBPlugin \ No newline at end of file diff --git a/chat2db-server/chat2db-plugins/chat2db-h2/src/main/java/ai/chat2db/plugin/h2/H2Meta.java b/chat2db-server/chat2db-plugins/chat2db-h2/src/main/java/ai/chat2db/plugin/h2/H2Meta.java index 1b1cc2fd6..05f938c3b 100644 --- a/chat2db-server/chat2db-plugins/chat2db-h2/src/main/java/ai/chat2db/plugin/h2/H2Meta.java +++ b/chat2db-server/chat2db-plugins/chat2db-h2/src/main/java/ai/chat2db/plugin/h2/H2Meta.java @@ -3,6 +3,7 @@ import java.sql.Connection; import java.sql.ResultSet; import java.sql.ResultSetMetaData; +import java.sql.SQLException; import java.util.*; import java.util.stream.Collectors; diff --git a/chat2db-server/chat2db-plugins/chat2db-hive/src/main/java/ai/chat2db/plugin/hive/builder/HiveSqlBuilder.java b/chat2db-server/chat2db-plugins/chat2db-hive/src/main/java/ai/chat2db/plugin/hive/builder/HiveSqlBuilder.java index 02a761df7..a3ca677dd 100644 --- a/chat2db-server/chat2db-plugins/chat2db-hive/src/main/java/ai/chat2db/plugin/hive/builder/HiveSqlBuilder.java +++ b/chat2db-server/chat2db-plugins/chat2db-hive/src/main/java/ai/chat2db/plugin/hive/builder/HiveSqlBuilder.java @@ -2,6 +2,7 @@ import ai.chat2db.plugin.hive.type.HiveColumnTypeEnum; import ai.chat2db.plugin.hive.type.HiveIndexTypeEnum; +import ai.chat2db.spi.SqlBuilder; import ai.chat2db.spi.jdbc.DefaultSqlBuilder; import ai.chat2db.spi.model.Database; import ai.chat2db.spi.model.Table; @@ -9,6 +10,7 @@ import ai.chat2db.spi.model.TableIndex; import org.apache.commons.lang3.StringUtils; +import java.util.List; public class HiveSqlBuilder extends DefaultSqlBuilder implements SqlBuilder
{ diff --git a/chat2db-server/chat2db-plugins/pom.xml b/chat2db-server/chat2db-plugins/pom.xml index a98b555cb..878eddd17 100644 --- a/chat2db-server/chat2db-plugins/pom.xml +++ b/chat2db-server/chat2db-plugins/pom.xml @@ -30,6 +30,7 @@ chat2db-hivechat2db-kingbasechat2db-timeplus + chat2db-duckdb diff --git a/chat2db-server/chat2db-server-domain/chat2db-server-domain-core/pom.xml b/chat2db-server/chat2db-server-domain/chat2db-server-domain-core/pom.xml index 39d5c2a2d..7fdb052b2 100644 --- a/chat2db-server/chat2db-server-domain/chat2db-server-domain-core/pom.xml +++ b/chat2db-server/chat2db-server-domain/chat2db-server-domain-core/pom.xml @@ -126,6 +126,11 @@ chat2db-timeplus${revision} + + ai.chat2db + chat2db-duckdb + ${version} + commons-codec commons-codec diff --git a/chat2db-server/chat2db-server-domain/chat2db-server-domain-core/src/main/java/ai/chat2db/server/domain/core/impl/TableServiceImpl.java b/chat2db-server/chat2db-server-domain/chat2db-server-domain-core/src/main/java/ai/chat2db/server/domain/core/impl/TableServiceImpl.java index 18038e95a..9834cdf10 100644 --- a/chat2db-server/chat2db-server-domain/chat2db-server-domain-core/src/main/java/ai/chat2db/server/domain/core/impl/TableServiceImpl.java +++ b/chat2db-server/chat2db-server-domain/chat2db-server-domain-core/src/main/java/ai/chat2db/server/domain/core/impl/TableServiceImpl.java @@ -424,7 +424,7 @@ private long addDBCache(Long dataSourceId, String databaseName, String schemaNam Connection connection = Chat2DBContext.getConnection(); long n = 0; try (ResultSet resultSet = connection.getMetaData().getTables(databaseName, schemaName, null, - new String[]{"TABLE", "SYSTEM TABLE"})) { + new String[]{"TABLE", "SYSTEM TABLE", "BASE TABLE"})) { List cacheDOS = new ArrayList<>(); while (resultSet.next()) { TableCacheDO tableCacheDO = new TableCacheDO(); diff --git a/chat2db-server/chat2db-server-domain/chat2db-server-domain-repository/src/main/resources/db/migration/V2_1_10__REMOVEdEMO.sql b/chat2db-server/chat2db-server-domain/chat2db-server-domain-repository/src/main/resources/db/migration/V2_1_10__REMOVEdEMO.sql new file mode 100644 index 000000000..b0f5f6300 --- /dev/null +++ b/chat2db-server/chat2db-server-domain/chat2db-server-domain-repository/src/main/resources/db/migration/V2_1_10__REMOVEdEMO.sql @@ -0,0 +1,7 @@ +delete from DATA_SOURCE where ALIAS ='DEMO@db.sqlgpt.cn'; + +delete from DASHBOARD where id =ID; + +delete from CHART where id<=3; + +delete from DASHBOARD_CHART_RELATION where CHART_ID<=3; diff --git a/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/rdb/converter/RdbWebConverter.java b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/rdb/converter/RdbWebConverter.java index f9c529a5b..7881cf93a 100644 --- a/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/rdb/converter/RdbWebConverter.java +++ b/chat2db-server/chat2db-server-web/chat2db-server-web-api/src/main/java/ai/chat2db/server/web/api/controller/rdb/converter/RdbWebConverter.java @@ -42,6 +42,8 @@ public abstract class RdbWebConverter { public abstract DlExecuteParam request2param(DmlRequest request); + + public abstract GroupByParam request2param(GroupByRequest request); /** * Parameter conversion *