diff --git a/agents/common/src/main/java/com/dbx/agent/AgentProtocol.java b/agents/common/src/main/java/com/dbx/agent/AgentProtocol.java index d38063ccd2..a04f7eac3a 100644 --- a/agents/common/src/main/java/com/dbx/agent/AgentProtocol.java +++ b/agents/common/src/main/java/com/dbx/agent/AgentProtocol.java @@ -15,6 +15,7 @@ public final class AgentProtocol { public static final String METHOD_LIST_SCHEMAS = "list_schemas"; public static final String METHOD_LIST_TABLES = "list_tables"; public static final String METHOD_LIST_OBJECTS = "list_objects"; + public static final String METHOD_LIST_DATA_TYPES = "list_data_types"; public static final String METHOD_COMPLETION_ASSISTANT_SEARCH_V1 = "completion_assistant_search_v1"; public static final String METHOD_GET_OBJECT_SOURCE = "get_object_source"; public static final String METHOD_GET_TABLE_DDL = "get_table_ddl"; @@ -85,6 +86,7 @@ public final class AgentProtocol { METHOD_LIST_SCHEMAS, METHOD_LIST_TABLES, METHOD_LIST_OBJECTS, + METHOD_LIST_DATA_TYPES, METHOD_COMPLETION_ASSISTANT_SEARCH_V1, METHOD_GET_OBJECT_SOURCE, METHOD_GET_TABLE_DDL, diff --git a/agents/common/src/main/java/com/dbx/agent/ConfiguredJdbcAgent.java b/agents/common/src/main/java/com/dbx/agent/ConfiguredJdbcAgent.java index d6addc56cc..7bc7d55de2 100644 --- a/agents/common/src/main/java/com/dbx/agent/ConfiguredJdbcAgent.java +++ b/agents/common/src/main/java/com/dbx/agent/ConfiguredJdbcAgent.java @@ -60,6 +60,11 @@ public abstract class ConfiguredJdbcAgent extends AbstractJdbcAgent { return StandardJdbcMetadata.INSTANCE.listObjects(listTables(schema), schema); } + @Override + public List listDataTypes() { + return StandardJdbcMetadata.INSTANCE.listDataTypes(requireConnection()); + } + @Override public CompletionAssistantResponse completionAssistantSearch(CompletionAssistantRequest request) { return StandardJdbcMetadata.INSTANCE.completionAssistantSearch(requireConnection(), profile, configuredDatabase, request); diff --git a/agents/common/src/main/java/com/dbx/agent/DatabaseAgent.java b/agents/common/src/main/java/com/dbx/agent/DatabaseAgent.java index 18f72b4d2c..bd3f410577 100644 --- a/agents/common/src/main/java/com/dbx/agent/DatabaseAgent.java +++ b/agents/common/src/main/java/com/dbx/agent/DatabaseAgent.java @@ -35,6 +35,10 @@ public interface DatabaseAgent { return result; } + default List listDataTypes() { + return Collections.emptyList(); + } + default CompletionAssistantResponse completionAssistantSearch(CompletionAssistantRequest request) { throw new UnsupportedOperationException("Completion assistant search is not supported by this agent"); } diff --git a/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java b/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java index 72d5c644eb..3d3aed70a9 100644 --- a/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java +++ b/agents/common/src/main/java/com/dbx/agent/JsonRpcServer.java @@ -115,6 +115,10 @@ public final class JsonRpcServer { switchCatalog(params); return agent.listObjects(params.get("schema").getAsString()); } + if (AgentProtocol.METHOD_LIST_DATA_TYPES.equals(method)) { + switchCatalog(params); + return agent.listDataTypes(); + } if (AgentProtocol.METHOD_COMPLETION_ASSISTANT_SEARCH_V1.equals(method)) { switchCatalog(params); return agent.completionAssistantSearch(gson.fromJson(params, CompletionAssistantRequest.class)); diff --git a/agents/common/src/main/java/com/dbx/agent/StandardJdbcMetadata.java b/agents/common/src/main/java/com/dbx/agent/StandardJdbcMetadata.java index 5090e1a12a..478dd6b4cb 100644 --- a/agents/common/src/main/java/com/dbx/agent/StandardJdbcMetadata.java +++ b/agents/common/src/main/java/com/dbx/agent/StandardJdbcMetadata.java @@ -90,6 +90,26 @@ public final class StandardJdbcMetadata { return result; } + public List listDataTypes(Connection conn) { + return unchecked(() -> { + Set seen = new LinkedHashSet<>(); + List names = new ArrayList<>(); + try (ResultSet rs = conn.getMetaData().getTypeInfo()) { + while (rs.next()) { + String name = rs.getString("TYPE_NAME"); + if (name == null || name.trim().isEmpty()) { + continue; + } + String trimmed = name.trim(); + if (seen.add(trimmed.toLowerCase(Locale.ROOT))) { + names.add(trimmed); + } + } + } + return names; + }); + } + public CompletionAssistantResponse completionAssistantSearch( Connection conn, JdbcAgentProfile profile, diff --git a/agents/common/src/main/resources/agent-protocol-v1.json b/agents/common/src/main/resources/agent-protocol-v1.json index 77d03637fe..10ea69f175 100644 --- a/agents/common/src/main/resources/agent-protocol-v1.json +++ b/agents/common/src/main/resources/agent-protocol-v1.json @@ -43,6 +43,7 @@ "list_schemas", "list_tables", "list_objects", + "list_data_types", "completion_assistant_search_v1", "get_object_source", "get_table_ddl", diff --git a/agents/common/src/test/java/com/dbx/agent/StandardJdbcMetadataTest.java b/agents/common/src/test/java/com/dbx/agent/StandardJdbcMetadataTest.java index 2db920bece..40bbbf95d0 100644 --- a/agents/common/src/test/java/com/dbx/agent/StandardJdbcMetadataTest.java +++ b/agents/common/src/test/java/com/dbx/agent/StandardJdbcMetadataTest.java @@ -108,6 +108,26 @@ class StandardJdbcMetadataTest { assertEquals("TABLE", tables.get(1).getTable_type()); } + @Test + void listsDataTypesFromJdbcTypeInfo() { + Connection conn = connection( + rows(), + rows(), + rows(), + rows(), + rows(), + rows(), + UnsupportedSchemaCall.NONE, + null, + null, + null, + null, + rows(row("TYPE_NAME", "INTEGER"), row("TYPE_NAME", "VARCHAR"), row("TYPE_NAME", "integer")) + ); + + assertEquals(Arrays.asList("INTEGER", "VARCHAR"), StandardJdbcMetadata.INSTANCE.listDataTypes(conn)); + } + @Test void listTablesUsesDriverTableTypesWithinProfileAllowList() { AtomicReference capturedTypes = new AtomicReference<>(); @@ -369,6 +389,36 @@ class StandardJdbcMetadataTest { AtomicReference capturedTableTypes, AtomicReference capturedTableArgs, AtomicReference capturedColumnArgs + ) { + return connection( + schemas, + tables, + primaryKeys, + columns, + indexes, + foreignKeys, + unsupportedSchemaCall, + tableTypes, + capturedTableTypes, + capturedTableArgs, + capturedColumnArgs, + null + ); + } + + private static Connection connection( + ResultSet schemas, + ResultSet tables, + ResultSet primaryKeys, + ResultSet columns, + ResultSet indexes, + ResultSet foreignKeys, + UnsupportedSchemaCall unsupportedSchemaCall, + ResultSet tableTypes, + AtomicReference capturedTableTypes, + AtomicReference capturedTableArgs, + AtomicReference capturedColumnArgs, + ResultSet dataTypes ) { DatabaseMetaData meta = proxy(DatabaseMetaData.class, new MethodHandler() { @Override @@ -392,6 +442,9 @@ class StandardJdbcMetadataTest { if ("getTableTypes".equals(name) && tableTypes != null) { return tableTypes; } + if ("getTypeInfo".equals(name) && dataTypes != null) { + return dataTypes; + } if ("getPrimaryKeys".equals(name)) { return primaryKeys; } diff --git a/agents/drivers/xugu/main.go b/agents/drivers/xugu/main.go index 30eea84940..c27a6dbf50 100644 --- a/agents/drivers/xugu/main.go +++ b/agents/drivers/xugu/main.go @@ -54,6 +54,36 @@ WHERE UPPER(s.SCHEMA_NAME) = UPPER(?) AND UPPER(t.TABLE_NAME) = UPPER(?) ORDER BY i.INDEX_NAME` +var xuguDataTypes = []string{ + "BOOLEAN", + "INTEGER", + "SMALLINT", + "BIGINT", + "FLOAT", + "NUMERIC", + "CHAR", + "VARCHAR", + "CLOB", + "DATE", + "TIME", + "TIMESTAMP", + "BINARY", + "VARBINARY", + "BLOB", + "XML", + "BOOL", + "INT", + "SHORT", + "LONGINT", + "LONG", + "REAL", + "DECIMAL", + "TEXT", + "NCHAR", + "NVARCHAR", + "NVARCHAR2", +} + type request struct { ID json.RawMessage `json:"id"` Method string `json:"method"` @@ -320,6 +350,8 @@ func (s *server) dispatch(method string, params map[string]json.RawMessage) (any schema := stringParam(params, "schema") result, err := s.listObjects(schema) return result, false, err + case "list_data_types": + return xuguDataTypes, false, nil case "get_columns": if err := s.useDatabase(stringParam(params, "database")); err != nil { return nil, false, err diff --git a/agents/drivers/xugu/main_test.go b/agents/drivers/xugu/main_test.go index 3402d7f386..b32c3f11a7 100644 --- a/agents/drivers/xugu/main_test.go +++ b/agents/drivers/xugu/main_test.go @@ -60,6 +60,30 @@ func TestCloseMissingQuerySessionReturnsFalse(t *testing.T) { } } +func TestListDataTypesReturnsXuguTypes(t *testing.T) { + s := newServer() + resp, shutdown := s.handleLine(`{"jsonrpc":"2.0","id":9,"method":"list_data_types","params":{"database":"demo"}}`) + if shutdown { + t.Fatal("list_data_types should not shut down the server") + } + if resp.Error != nil { + t.Fatalf("unexpected error: %v", resp.Error) + } + data, err := json.Marshal(resp.Result) + if err != nil { + t.Fatal(err) + } + var result []string + if err := json.Unmarshal(data, &result); err != nil { + t.Fatal(err) + } + for _, want := range []string{"INTEGER", "VARCHAR", "NUMERIC", "INT"} { + if !contains(result, want) { + t.Fatalf("expected data type %q in %v", want, result) + } + } +} + func TestEmptyResultSlicesMarshalAsArrays(t *testing.T) { data, err := json.Marshal(queryResult{}) if err != nil { diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index 74972ee2e2..aa9ea51434 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -56,6 +56,7 @@ const historyStore = useHistoryStore(); const settingsStore = useSettingsStore(); const { toast } = useToast(); const rootRef = ref(); +const dynamicDataTypeOptionsCache = new Map(); const sqlHighlighter = ref(); onMounted(async () => { @@ -454,7 +455,8 @@ const structureCapabilities = computed(() => getTableStructureCapabilities(datab const tableMetadataCapabilities = computed(() => getTableMetadataCapabilities(databaseType.value)); const structureDialect = computed(() => structureCapabilities.value.dialect); const isTableCommentDisabled = computed(() => !structureCapabilities.value.comment); -const dataTypeOptions = computed(() => getDataTypeOptions(databaseType.value)); +const dynamicDataTypeOptions = ref([]); +const dataTypeOptions = computed(() => mergeDataTypeOptions(dynamicDataTypeOptions.value, getDataTypeOptions(databaseType.value))); const columnEditorControls = computed(() => getColumnEditorControls(databaseType.value)); const indexTypesByDb: Record = { @@ -587,6 +589,7 @@ function isManticoreJsonColumn(column: EditableStructureColumn): boolean { let sqlPreviewRequestId = 0; let structureLoadRequestId = 0; +let dataTypeOptionsRequestId = 0; let sqlPreviewDebounceTimer: ReturnType | undefined; let deferredSqlPreviewRefresh = false; let keydownListenerRegistered = false; @@ -612,6 +615,57 @@ function clearSqlPreviewState() { warnings.value = []; } +function dataTypeOptionsCacheKey(connectionId: string, database: string) { + return `${connectionId}\u0000${database}`; +} + +function mergeDataTypeOptions(primary: readonly string[], fallback: readonly string[]): string[] { + const seen = new Set(); + const result: string[] = []; + for (const option of [...primary, ...fallback]) { + const trimmed = option.trim(); + if (!trimmed) continue; + const key = trimmed.toLowerCase(); + if (seen.has(key)) continue; + seen.add(key); + result.push(trimmed); + } + return result; +} + +async function loadDynamicDataTypeOptions() { + const requestId = ++dataTypeOptionsRequestId; + const connectionId = props.connectionId; + const database = props.database; + if (!connectionId || !database) { + dynamicDataTypeOptions.value = []; + return; + } + const cacheKey = dataTypeOptionsCacheKey(connectionId, database); + const cached = dynamicDataTypeOptionsCache.get(cacheKey); + if (cached) { + dynamicDataTypeOptions.value = cached; + return; + } + dynamicDataTypeOptions.value = []; + try { + await store.ensureConnected(connectionId); + const options = await api.listDataTypes(connectionId, database); + if (requestId !== dataTypeOptionsRequestId) return; + const normalized = mergeDataTypeOptions(options, []); + if (normalized.length > 0) { + dynamicDataTypeOptionsCache.set(cacheKey, normalized); + dynamicDataTypeOptions.value = normalized; + } else { + dynamicDataTypeOptions.value = []; + } + } catch { + if (requestId === dataTypeOptionsRequestId) { + dynamicDataTypeOptions.value = []; + } + } +} + function scheduleSqlPreviewRefresh() { if (!hasPendingStructureChanges()) { clearSqlPreviewState(); @@ -1222,11 +1276,13 @@ function unregisterStructureEditorShortcuts() { onMounted(() => { resetState(); registerStructureEditorShortcuts(); + void loadDynamicDataTypeOptions(); void loadStructure(); }); onActivated(() => { registerStructureEditorShortcuts(); + void loadDynamicDataTypeOptions(); if (!isCreateMode.value) void loadStructure(true); }); onDeactivated(unregisterStructureEditorShortcuts); @@ -1255,6 +1311,10 @@ watch(tableMetadataCapabilities, (capabilities) => { if (!supported) activeTab.value = firstStructureMetadataTab(capabilities); }); +watch([() => props.connectionId, () => props.database, databaseType], () => { + void loadDynamicDataTypeOptions(); +}); + watch( [isCreateMode, databaseType, () => props.schema, () => props.tableName, newTableName, tableComment, columns, indexes, foreignKeys, triggers], () => { diff --git a/apps/desktop/src/lib/api.ts b/apps/desktop/src/lib/api.ts index 5ea3c25ef4..4340c73977 100644 --- a/apps/desktop/src/lib/api.ts +++ b/apps/desktop/src/lib/api.ts @@ -122,6 +122,7 @@ export const listCompletionObjects = forward("listCompletionObjects"); export const completionAssistantSearch = forward("completionAssistantSearch"); export const getObjectSource = forward("getObjectSource"); export const getColumns = forward("getColumns"); +export const listDataTypes = forward("listDataTypes"); export const listIndexes = forward("listIndexes"); export const listForeignKeys = forward("listForeignKeys"); export const listTriggers = forward("listTriggers"); diff --git a/apps/desktop/src/lib/http.ts b/apps/desktop/src/lib/http.ts index 6c02e6a36f..60badf4727 100644 --- a/apps/desktop/src/lib/http.ts +++ b/apps/desktop/src/lib/http.ts @@ -527,6 +527,10 @@ export async function getColumns(connectionId: string, database: string, schema: return get(`/api/schema/columns?${qs({ connection_id: connectionId, database, schema, table })}`); } +export async function listDataTypes(connectionId: string, database: string): Promise { + return get(`/api/schema/data-types?${qs({ connection_id: connectionId, database })}`); +} + export async function listIndexes(connectionId: string, database: string, schema: string, table: string): Promise { return get(`/api/schema/indexes?${qs({ connection_id: connectionId, database, schema, table })}`); } diff --git a/apps/desktop/src/lib/tableStructureEditorState.ts b/apps/desktop/src/lib/tableStructureEditorState.ts index 423736451c..804fa52a3a 100644 --- a/apps/desktop/src/lib/tableStructureEditorState.ts +++ b/apps/desktop/src/lib/tableStructureEditorState.ts @@ -268,6 +268,7 @@ export const DATA_TYPE_OPTIONS: Record = { "interval day to second", ], questdb: ["boolean", "ipv4", "byte", "short", "char", "int", "float", "symbol", "varchar", "string", "long", "date", "timestamp", "timestamp_ns", "double", "uuid", "binary", "long256", "geohash", "array", "interval", "decimal"], + xugu: ["BOOLEAN", "INTEGER", "SMALLINT", "BIGINT", "FLOAT", "NUMERIC", "CHAR", "VARCHAR", "CLOB", "DATE", "TIME", "TIMESTAMP", "BINARY", "VARBINARY", "BLOB", "XML", "BOOL", "INT", "SHORT", "LONGINT", "LONG", "REAL", "DECIMAL", "TEXT", "NCHAR", "NVARCHAR", "NVARCHAR2"], }; const DATA_TYPE_OPTION_ALIASES: Partial> = { diff --git a/apps/desktop/src/lib/tauri.ts b/apps/desktop/src/lib/tauri.ts index 5690a94a07..f69a93de41 100644 --- a/apps/desktop/src/lib/tauri.ts +++ b/apps/desktop/src/lib/tauri.ts @@ -569,6 +569,10 @@ export async function getColumns(connectionId: string, database: string, schema: return invoke("get_columns", { connectionId, database, schema, table }); } +export async function listDataTypes(connectionId: string, database: string): Promise { + return invoke("list_data_types", { connectionId, database }); +} + export async function executeQuery( connectionId: string, database: string, diff --git a/crates/dbx-core/assets/agent-protocol-v1.json b/crates/dbx-core/assets/agent-protocol-v1.json index 77d03637fe..10ea69f175 100644 --- a/crates/dbx-core/assets/agent-protocol-v1.json +++ b/crates/dbx-core/assets/agent-protocol-v1.json @@ -43,6 +43,7 @@ "list_schemas", "list_tables", "list_objects", + "list_data_types", "completion_assistant_search_v1", "get_object_source", "get_table_ddl", diff --git a/crates/dbx-core/src/db/agent_driver.rs b/crates/dbx-core/src/db/agent_driver.rs index ca5132368b..918176a208 100644 --- a/crates/dbx-core/src/db/agent_driver.rs +++ b/crates/dbx-core/src/db/agent_driver.rs @@ -120,6 +120,7 @@ pub enum AgentMethod { ListSchemas, ListTables, ListObjects, + ListDataTypes, CompletionAssistantSearchV1, GetObjectSource, GetColumns, @@ -141,7 +142,7 @@ pub enum AgentMethod { } impl AgentMethod { - pub const ALL: [Self; 26] = [ + pub const ALL: [Self; 27] = [ Self::Handshake, Self::Connect, Self::TestConnection, @@ -150,6 +151,7 @@ impl AgentMethod { Self::ListSchemas, Self::ListTables, Self::ListObjects, + Self::ListDataTypes, Self::CompletionAssistantSearchV1, Self::GetObjectSource, Self::GetTableDdl, @@ -180,6 +182,7 @@ impl AgentMethod { Self::ListSchemas => "list_schemas", Self::ListTables => "list_tables", Self::ListObjects => "list_objects", + Self::ListDataTypes => "list_data_types", Self::CompletionAssistantSearchV1 => "completion_assistant_search_v1", Self::GetObjectSource => "get_object_source", Self::GetTableDdl => "get_table_ddl", @@ -616,6 +619,19 @@ impl AgentDriverClient { .await } + pub async fn list_data_types( + &mut self, + database: &str, + timeout_duration: Option, + ) -> Result { + self.call_method_with_timeout( + AgentMethod::ListDataTypes, + serde_json::json!({ "database": database }), + timeout_duration, + ) + .await + } + pub async fn completion_assistant_search( &mut self, request: &crate::types::CompletionAssistantRequest, @@ -1338,6 +1354,7 @@ mod tests { assert_eq!(AgentMethod::ListSchemas.as_str(), "list_schemas"); assert_eq!(AgentMethod::ListTables.as_str(), "list_tables"); assert_eq!(AgentMethod::ListObjects.as_str(), "list_objects"); + assert_eq!(AgentMethod::ListDataTypes.as_str(), "list_data_types"); assert_eq!(AgentMethod::CompletionAssistantSearchV1.as_str(), "completion_assistant_search_v1"); assert_eq!(AgentMethod::GetObjectSource.as_str(), "get_object_source"); assert_eq!(AgentMethod::GetColumns.as_str(), "get_columns"); diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs index 756e012f76..bbf2a73221 100644 --- a/crates/dbx-core/src/schema.rs +++ b/crates/dbx-core/src/schema.rs @@ -627,6 +627,56 @@ async fn list_schema_infos_once( Ok(schemas.into_iter().map(|name| db::SchemaInfo { name, comment: None }).collect()) } +pub async fn list_data_types_core( + state: &AppState, + connection_id: &str, + database: &str, +) -> Result, String> { + retry_metadata_connection(state, connection_id, Some(database), || async { + let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?; + let db_config = connection_config(state, connection_id).await; + let connections = state.connections.read().await; + if let Some(PoolKind::ExternalDriver { config, session, .. }) = connections.get(&pool_key) { + let config = config.clone(); + let session = session.clone(); + drop(connections); + return session + .invoke::>( + "listDataTypes", + serde_json::json!({ "connection": config.as_ref(), "database": database }), + ) + .await + .map(deduplicate_data_type_names); + } + if let Some(client) = extract_pool!(&connections, &pool_key, Agent) { + drop(connections); + let mut client = client.lock().await; + return client + .list_data_types::>(database, agent_metadata_timeout(db_config.as_ref())) + .await + .map(deduplicate_data_type_names); + } + Ok(Vec::new()) + }) + .await +} + +fn deduplicate_data_type_names(names: Vec) -> Vec { + let mut seen = std::collections::HashSet::new(); + let mut result = Vec::new(); + for name in names { + let trimmed = name.trim(); + if trimmed.is_empty() { + continue; + } + let key = trimmed.to_ascii_lowercase(); + if seen.insert(key) { + result.push(trimmed.to_string()); + } + } + result +} + async fn list_schemas_once( state: &AppState, connection_id: &str, diff --git a/crates/dbx-web/src/main.rs b/crates/dbx-web/src/main.rs index d39d01e356..2dc93a4ba9 100644 --- a/crates/dbx-web/src/main.rs +++ b/crates/dbx-web/src/main.rs @@ -207,6 +207,7 @@ async fn main() { .route("/schema/completion-assistant", post(routes::schema::completion_assistant_search)) .route("/schema/object-source", get(routes::schema::get_object_source)) .route("/schema/columns", get(routes::schema::list_columns)) + .route("/schema/data-types", get(routes::schema::list_data_types)) .route("/schema/indexes", get(routes::schema::list_indexes)) .route("/schema/foreign-keys", get(routes::schema::list_foreign_keys)) .route("/schema/triggers", get(routes::schema::list_triggers)) diff --git a/crates/dbx-web/src/routes/schema.rs b/crates/dbx-web/src/routes/schema.rs index c579ed601c..4df123b2b1 100644 --- a/crates/dbx-web/src/routes/schema.rs +++ b/crates/dbx-web/src/routes/schema.rs @@ -197,6 +197,16 @@ pub async fn list_columns( Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?)) } +pub async fn list_data_types( + State(state): State>, + Query(q): Query, +) -> Result, AppError> { + let database = q.database.as_deref().unwrap_or(""); + let result = + dbx_core::schema::list_data_types_core(&state.app, &q.connection_id, database).await.map_err(AppError)?; + Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?)) +} + pub async fn list_indexes( State(state): State>, Query(q): Query, diff --git a/packages/app-tests/tableStructureEditorState.test.ts b/packages/app-tests/tableStructureEditorState.test.ts index 359e62410f..7928c061ae 100644 --- a/packages/app-tests/tableStructureEditorState.test.ts +++ b/packages/app-tests/tableStructureEditorState.test.ts @@ -1,6 +1,21 @@ import assert from "node:assert/strict"; import { test } from "vitest"; -import { applyManticoreDdlColumnExtras, buildStructureTargetLabel, canEditManticoreColumnProperties, combineDataTypeForDatabase, createColumnDrafts, createIndexDrafts, generateIndexName, generateUniqueIndexName, getColumnEditorControls, getDataTypeOptions, isProtectedManticoreIdColumn, normalizeDataTypeParams, parseExtraToColumnExtra, toColumnNames } from "../../apps/desktop/src/lib/tableStructureEditorState.ts"; +import { + applyManticoreDdlColumnExtras, + buildStructureTargetLabel, + canEditManticoreColumnProperties, + combineDataTypeForDatabase, + createColumnDrafts, + createIndexDrafts, + generateIndexName, + generateUniqueIndexName, + getColumnEditorControls, + getDataTypeOptions, + isProtectedManticoreIdColumn, + normalizeDataTypeParams, + parseExtraToColumnExtra, + toColumnNames, +} from "../../apps/desktop/src/lib/tableStructureEditorState.ts"; import type { ColumnInfo, IndexInfo } from "../../apps/desktop/src/types/database.ts"; const columns: ColumnInfo[] = [ @@ -223,6 +238,14 @@ test("returns data type options for compatible table structure editors", () => { assert.equal(getDataTypeOptions("sqlserver").includes("nvarchar"), true); }); +test("returns Xugu data type options", () => { + const options = getDataTypeOptions("xugu"); + assert.equal(options.includes("INTEGER"), true); + assert.equal(options.includes("VARCHAR"), true); + assert.equal(options.includes("NUMERIC"), true); + assert.equal(options.includes("INT"), true); +}); + test("returns Manticore Search data type options", () => { assert.deepEqual(getDataTypeOptions("manticoresearch"), ["text", "string", "int", "bit", "bigint", "bool", "timestamp", "float", "json", "float_vector", "multi", "mva"]); }); diff --git a/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java b/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java index 599734f939..285a99628d 100644 --- a/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java +++ b/plugins/jdbc/src/main/java/app/dbx/jdbc/DbxJdbcPlugin.java @@ -262,6 +262,7 @@ public final class DbxJdbcPlugin { optionalText(params, "database"), optionalText(params, "schema") ); + case "listDataTypes", "list_data_types" -> listDataTypes(connection, optionalText(params, "database")); case "getObjectSource", "get_object_source" -> getObjectSource( connection, optionalText(params, "database"), @@ -1163,6 +1164,33 @@ public final class DbxJdbcPlugin { return result; } + private static JsonNode listDataTypes(JsonNode connection, String database) throws SQLException { + Connection conn = openConnection(connection); + JdbcDriverQuirks quirks = driverQuirks(connection); + String catalog = metadataCatalog(database, quirks); + if (catalog != null) { + try { + conn.setCatalog(catalog); + } catch (SQLFeatureNotSupportedException | AbstractMethodError | UnsupportedOperationException ignored) { + } + } + ArrayNode result = MAPPER.createArrayNode(); + Set seen = new HashSet<>(); + try (ResultSet rs = conn.getMetaData().getTypeInfo()) { + while (rs.next()) { + String name = rs.getString("TYPE_NAME"); + if (name == null || name.isBlank()) { + continue; + } + String trimmed = name.trim(); + if (seen.add(trimmed.toLowerCase(Locale.ROOT))) { + result.add(trimmed); + } + } + } + return result; + } + private static JsonNode getColumns(JsonNode connection, String database, String schema, String table) throws SQLException { ArrayNode result = MAPPER.createArrayNode(); Connection conn = openConnection(connection); diff --git a/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java b/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java index 0cf9bd8706..1af882c543 100644 --- a/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java +++ b/plugins/jdbc/src/test/java/app/dbx/jdbc/DbxJdbcPluginTest.java @@ -604,6 +604,28 @@ final class DbxJdbcPluginTest { assertEquals(true, found); } + @Test + void listDataTypesUsesJdbcTypeInfo() throws Exception { + JsonNode response = request("listDataTypes", """ + { "connection": %s } + """.formatted(CONNECTION)); + + assertFalse(response.has("error"), response.toString()); + boolean foundInteger = false; + boolean foundVarchar = false; + for (JsonNode type : response.path("result")) { + String name = type.asText(); + if ("INTEGER".equalsIgnoreCase(name)) { + foundInteger = true; + } + if ("VARCHAR".equalsIgnoreCase(name) || "CHARACTER VARYING".equalsIgnoreCase(name)) { + foundVarchar = true; + } + } + assertEquals(true, foundInteger); + assertEquals(true, foundVarchar); + } + @Test void listObjectsAcceptsCamelCaseMethodAndFallsBackWhenCatalogFiltersEverything() throws Exception { createPeopleTable(); diff --git a/src-tauri/src/commands/schema.rs b/src-tauri/src/commands/schema.rs index da3f3dbce3..cdb7b372df 100644 --- a/src-tauri/src/commands/schema.rs +++ b/src-tauri/src/commands/schema.rs @@ -88,6 +88,15 @@ pub async fn list_schema_infos( dbx_core::schema::list_schema_infos_core(&state, &connection_id, &database).await } +#[tauri::command] +pub async fn list_data_types( + state: State<'_, Arc>, + connection_id: String, + database: String, +) -> Result, String> { + dbx_core::schema::list_data_types_core(&state, &connection_id, &database).await +} + #[tauri::command] pub async fn list_tables( state: State<'_, Arc>, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 890b2166ac..405c3042f1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -629,6 +629,7 @@ pub fn run() { commands::schema::get_object_source, commands::schema::list_schemas, commands::schema::list_schema_infos, + commands::schema::list_data_types, commands::schema::get_columns, commands::schema::list_indexes, commands::schema::list_foreign_keys,