The data grid header only showed column names, and column types were available only via tableMeta (open-table view). Arbitrary query results (e.g. `select * from pg_depend`) therefore showed no type at all, which is exactly the case the reporter hit. Backend: add `column_types` to QueryResult (serde-default, backward compatible) and populate it for the native drivers where the type is readily available — PostgreSQL, MySQL, SQL Server, ClickHouse. Other drivers leave it empty for now (no behavior change); schemaless stores (Mongo/Redis/ES) have no column types. Frontend: render a type row under each column name in the grid header, color-coded by type. The type is resolved from tableMeta first (richer, includes precision) and falls back to the query result's column_types by index. Add a `showColumnTypesInHeader` setting (default on) and keep the msgpack tab-result cache compatible. The source-selection logic is extracted to lib/dataGridColumnType.ts with unit tests. Co-authored-by: vrustx <vrustx@vrustxdeMac-mini.local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3b457f7803
commit
54de28d583
|
|
@ -116,6 +116,7 @@ const editAppLayout = ref(settingsStore.editorSettings.appLayout);
|
|||
const editShowTrayIcon = ref(settingsStore.desktopSettings.show_tray_icon);
|
||||
const editIconTheme = ref<DesktopIconTheme>(settingsStore.desktopSettings.icon_theme);
|
||||
const editShowColumnCommentsInHeader = ref(settingsStore.editorSettings.showColumnCommentsInHeader);
|
||||
const editShowColumnTypesInHeader = ref(settingsStore.editorSettings.showColumnTypesInHeader);
|
||||
const editCompactColumnHeaderActions = ref(settingsStore.editorSettings.compactColumnHeaderActions);
|
||||
const editRedisScanPageSize = ref(settingsStore.editorSettings.redisScanPageSize);
|
||||
const editShortcuts = ref(normalizeShortcutSettings(settingsStore.editorSettings.shortcuts));
|
||||
|
|
@ -273,6 +274,7 @@ watch(
|
|||
editShowTrayIcon.value = settingsStore.desktopSettings.show_tray_icon;
|
||||
editIconTheme.value = settingsStore.desktopSettings.icon_theme;
|
||||
editShowColumnCommentsInHeader.value = settingsStore.editorSettings.showColumnCommentsInHeader;
|
||||
editShowColumnTypesInHeader.value = settingsStore.editorSettings.showColumnTypesInHeader;
|
||||
editCompactColumnHeaderActions.value = settingsStore.editorSettings.compactColumnHeaderActions;
|
||||
editRedisScanPageSize.value = settingsStore.editorSettings.redisScanPageSize;
|
||||
editShortcuts.value = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
|
||||
|
|
@ -320,6 +322,7 @@ function hasChanges(): boolean {
|
|||
editShowTrayIcon.value !== settingsStore.desktopSettings.show_tray_icon ||
|
||||
editIconTheme.value !== settingsStore.desktopSettings.icon_theme ||
|
||||
editShowColumnCommentsInHeader.value !== settingsStore.editorSettings.showColumnCommentsInHeader ||
|
||||
editShowColumnTypesInHeader.value !== settingsStore.editorSettings.showColumnTypesInHeader ||
|
||||
editCompactColumnHeaderActions.value !== settingsStore.editorSettings.compactColumnHeaderActions ||
|
||||
editRedisScanPageSize.value !== settingsStore.editorSettings.redisScanPageSize ||
|
||||
JSON.stringify(editShortcuts.value) !== JSON.stringify(settingsStore.editorSettings.shortcuts) ||
|
||||
|
|
@ -354,6 +357,7 @@ async function persistSettings() {
|
|||
confirmDangerousSqlExecution: editConfirmDangerousSqlExecution.value,
|
||||
appLayout: editAppLayout.value,
|
||||
showColumnCommentsInHeader: editShowColumnCommentsInHeader.value,
|
||||
showColumnTypesInHeader: editShowColumnTypesInHeader.value,
|
||||
compactColumnHeaderActions: editCompactColumnHeaderActions.value,
|
||||
redisScanPageSize: editRedisScanPageSize.value,
|
||||
shortcuts: editShortcuts.value,
|
||||
|
|
@ -401,6 +405,7 @@ function resetDefaults() {
|
|||
editShowTrayIcon.value = DEFAULT_DESKTOP_SETTINGS.show_tray_icon;
|
||||
editIconTheme.value = DEFAULT_DESKTOP_SETTINGS.icon_theme;
|
||||
editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader;
|
||||
editShowColumnTypesInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader;
|
||||
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
|
||||
editRedisScanPageSize.value = DEFAULT_EDITOR_SETTINGS.redisScanPageSize;
|
||||
editShortcuts.value = normalizeShortcutSettings(DEFAULT_EDITOR_SETTINGS.shortcuts);
|
||||
|
|
@ -1569,6 +1574,17 @@ watch(
|
|||
</div>
|
||||
<Switch id="show-column-comments-in-header" v-model="editShowColumnCommentsInHeader" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="show-column-types-in-header">
|
||||
{{ t("settings.showColumnTypesInHeader") }}
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.showColumnTypesInHeaderDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch id="show-column-types-in-header" v-model="editShowColumnTypesInHeader" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="compact-column-header-actions">
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ import { createColumnDrafts } from "@/lib/tableStructureEditorState";
|
|||
import type { BuildSingleColumnAlterSqlOptions } from "@/lib/tableStructureEditorSql";
|
||||
import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql";
|
||||
import { uuid } from "@/lib/utils";
|
||||
import { resolveHeaderColumnType } from "@/lib/dataGridColumnType";
|
||||
import {
|
||||
canEditExistingTableRows,
|
||||
hiveTablePropertiesIndicateTransactional,
|
||||
|
|
@ -341,6 +342,7 @@ const columnCommentMap = computed(() => {
|
|||
return map;
|
||||
});
|
||||
const showColumnCommentsInHeader = computed(() => settingsStore.editorSettings.showColumnCommentsInHeader);
|
||||
const showColumnTypesInHeader = computed(() => settingsStore.editorSettings.showColumnTypesInHeader);
|
||||
const compactColumnHeaderActions = computed(() => settingsStore.editorSettings.compactColumnHeaderActions);
|
||||
const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode);
|
||||
|
||||
|
|
@ -349,6 +351,16 @@ function headerColumnComment(column: string): string {
|
|||
return columnCommentMap.value.get(column) || "";
|
||||
}
|
||||
|
||||
function headerColumnType(column: string, actualColIdx: number): string {
|
||||
if (!showColumnTypesInHeader.value) return "";
|
||||
const resolved = resolveHeaderColumnType({
|
||||
tableColumnType: columnTypeMap.value.get(column),
|
||||
resultColumnTypes: props.result.column_types,
|
||||
actualColIdx,
|
||||
});
|
||||
return resolved ? shortTypeName(resolved) : "";
|
||||
}
|
||||
|
||||
function shortTypeName(t: string): string {
|
||||
const s = t.toLowerCase();
|
||||
if (s === "character varying") return "varchar";
|
||||
|
|
@ -6505,6 +6517,14 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
<span class="min-w-0 truncate leading-4">
|
||||
{{ col.name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="headerColumnType(col.name, col.actualColIdx)"
|
||||
class="min-w-0 truncate text-[10px] font-normal leading-3"
|
||||
:class="typeColorClass(headerColumnType(col.name, col.actualColIdx))"
|
||||
:title="headerColumnType(col.name, col.actualColIdx)"
|
||||
>
|
||||
{{ headerColumnType(col.name, col.actualColIdx) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="headerColumnComment(col.name)"
|
||||
class="min-w-0 truncate text-[10px] font-normal leading-3 text-muted-foreground"
|
||||
|
|
|
|||
|
|
@ -1643,6 +1643,8 @@ export default {
|
|||
dataGridDisplay: "Data grid display",
|
||||
showColumnCommentsInHeader: "Show column comments under names",
|
||||
showColumnCommentsInHeaderDescription: "Display table column comments directly below grid column names.",
|
||||
showColumnTypesInHeader: "Show column types under names",
|
||||
showColumnTypesInHeaderDescription: "Display each column's data type directly below grid column names.",
|
||||
compactColumnHeaderActions: "Compact column header tools",
|
||||
compactColumnHeaderActionsDescription:
|
||||
"Move formatter and local filter tools into a more menu so column names get priority.",
|
||||
|
|
|
|||
|
|
@ -1530,6 +1530,9 @@ export default {
|
|||
showColumnCommentsInHeader: "Mostrar comentarios bajo los nombres",
|
||||
showColumnCommentsInHeaderDescription:
|
||||
"Muestra los comentarios de columnas directamente debajo del nombre de la columna.",
|
||||
showColumnTypesInHeader: "Mostrar tipos de columna bajo los nombres",
|
||||
showColumnTypesInHeaderDescription:
|
||||
"Muestra el tipo de dato de cada columna directamente debajo del nombre de la columna.",
|
||||
compactColumnHeaderActions: "Compactar herramientas del encabezado",
|
||||
compactColumnHeaderActionsDescription:
|
||||
"Mueve formatear y filtrar a un menú de más acciones para priorizar el nombre de columna.",
|
||||
|
|
|
|||
|
|
@ -1673,6 +1673,9 @@ export default {
|
|||
showColumnCommentsInHeader: "Mostra i commenti delle colonne sotto i nomi",
|
||||
showColumnCommentsInHeaderDescription:
|
||||
"Visualizza i commenti delle colonne della tabella direttamente sotto i nomi delle colonne nella griglia.",
|
||||
showColumnTypesInHeader: "Mostra i tipi di colonna sotto i nomi",
|
||||
showColumnTypesInHeaderDescription:
|
||||
"Visualizza il tipo di dato di ciascuna colonna direttamente sotto i nomi delle colonne nella griglia.",
|
||||
compactColumnHeaderActions: "Strumenti intestazione colonna compatti",
|
||||
compactColumnHeaderActionsDescription:
|
||||
"Sposta gli strumenti del formattatore e dei filtri locali in un menu 'altro' in modo che i nomi delle colonne abbiano la priorità.",
|
||||
|
|
|
|||
|
|
@ -1663,6 +1663,9 @@ export default {
|
|||
showColumnCommentsInHeader: "Mostrar comentários de coluna sob os nomes",
|
||||
showColumnCommentsInHeaderDescription:
|
||||
"Exibir comentários de colunas da tabela diretamente abaixo dos nomes das colunas da grade.",
|
||||
showColumnTypesInHeader: "Mostrar tipos de coluna sob os nomes",
|
||||
showColumnTypesInHeaderDescription:
|
||||
"Exibir o tipo de dado de cada coluna diretamente abaixo dos nomes das colunas da grade.",
|
||||
compactColumnHeaderActions: "Ferramentas compactas no cabeçalho da coluna",
|
||||
compactColumnHeaderActionsDescription:
|
||||
"Mover as ferramentas de formatação e filtro local para um menu de mais opções para que os nomes das colunas tenham prioridade.",
|
||||
|
|
|
|||
|
|
@ -1612,6 +1612,8 @@ export default {
|
|||
dataGridDisplay: "数据表格显示",
|
||||
showColumnCommentsInHeader: "在字段名下方显示注释",
|
||||
showColumnCommentsInHeaderDescription: "把表字段注释直接显示在结果表头字段名下方。",
|
||||
showColumnTypesInHeader: "在字段名下方显示数据类型",
|
||||
showColumnTypesInHeaderDescription: "把每个字段的数据类型直接显示在结果表头字段名下方。",
|
||||
compactColumnHeaderActions: "收起字段表头工具",
|
||||
compactColumnHeaderActionsDescription: "将格式化、本地筛选收进更多菜单,优先显示字段名称。",
|
||||
sidebarActivation: "侧边栏打开方式",
|
||||
|
|
|
|||
|
|
@ -1586,6 +1586,8 @@ export default {
|
|||
dataGridDisplay: "資料表格顯示",
|
||||
showColumnCommentsInHeader: "在欄位名稱下方顯示註解",
|
||||
showColumnCommentsInHeaderDescription: "直接在資料表格欄位名稱下方顯示資料表欄位註解。",
|
||||
showColumnTypesInHeader: "在欄位名稱下方顯示資料類型",
|
||||
showColumnTypesInHeaderDescription: "直接在資料表格欄位名稱下方顯示每個欄位的資料類型。",
|
||||
compactColumnHeaderActions: "收起欄位表頭工具",
|
||||
compactColumnHeaderActionsDescription: "將格式設定和本機篩選工具移到更多選單,讓欄位名稱優先顯示。",
|
||||
sidebarActivation: "側邊欄開啟方式",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* Resolve the data type to display in a data-grid column header.
|
||||
*
|
||||
* Two sources can supply a column's type:
|
||||
* - Table metadata (only when a table is open): matched **by column name**,
|
||||
* richer because it carries precision/scale. Preferred.
|
||||
* - `QueryResult.column_types` (any query): parallel to `result.columns`, so
|
||||
* it must be read **by index**. Used as a fallback for arbitrary queries
|
||||
* that have no table metadata (e.g. `select * from pg_depend`).
|
||||
*
|
||||
* Returns `undefined` when neither source has a non-empty type, so callers can
|
||||
* simply hide the type row.
|
||||
*/
|
||||
export interface HeaderColumnTypeSources {
|
||||
/** Type from table metadata for this column (looked up by name), if any. */
|
||||
tableColumnType?: string;
|
||||
/** `QueryResult.column_types`, parallel to `result.columns` (by index). */
|
||||
resultColumnTypes?: readonly string[];
|
||||
/** Index of the column within `result.columns`. */
|
||||
actualColIdx: number;
|
||||
}
|
||||
|
||||
export function resolveHeaderColumnType({
|
||||
tableColumnType,
|
||||
resultColumnTypes,
|
||||
actualColIdx,
|
||||
}: HeaderColumnTypeSources): string | undefined {
|
||||
const fromMeta = tableColumnType?.trim();
|
||||
if (fromMeta) return fromMeta;
|
||||
|
||||
const fromResult = resultColumnTypes?.[actualColIdx]?.trim();
|
||||
return fromResult ? fromResult : undefined;
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ export interface TabResultSnapshot {
|
|||
|
||||
interface ColumnarQueryResult {
|
||||
columns: string[];
|
||||
column_types?: string[];
|
||||
columnValues: CellValue[][];
|
||||
rowCount: number;
|
||||
affected_rows: number;
|
||||
|
|
@ -111,6 +112,7 @@ function stripSessionIds(result: QueryResult | undefined): QueryResult | undefin
|
|||
if (!result) return undefined;
|
||||
return {
|
||||
columns: [...result.columns],
|
||||
column_types: result.column_types ? [...result.column_types] : undefined,
|
||||
rows: result.rows.map((row) => [...row]),
|
||||
affected_rows: result.affected_rows,
|
||||
execution_time_ms: result.execution_time_ms,
|
||||
|
|
@ -129,6 +131,7 @@ function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult
|
|||
const columnValues = result.columns.map((_, colIndex) => result.rows.map((row) => row[colIndex] ?? null));
|
||||
return removeUndefinedFields({
|
||||
columns: [...result.columns],
|
||||
column_types: result.column_types ? [...result.column_types] : undefined,
|
||||
columnValues,
|
||||
rowCount: result.rows.length,
|
||||
affected_rows: result.affected_rows,
|
||||
|
|
@ -145,6 +148,7 @@ function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResul
|
|||
);
|
||||
return {
|
||||
columns: [...result.columns],
|
||||
column_types: result.column_types ? [...result.column_types] : undefined,
|
||||
rows,
|
||||
affected_rows: result.affected_rows,
|
||||
execution_time_ms: result.execution_time_ms,
|
||||
|
|
|
|||
|
|
@ -246,6 +246,7 @@ export interface EditorSettings {
|
|||
redisScanPageSize: number;
|
||||
mongoViewMode: "document" | "table";
|
||||
showColumnCommentsInHeader: boolean;
|
||||
showColumnTypesInHeader: boolean;
|
||||
compactColumnHeaderActions: boolean;
|
||||
dataGridRenderMode: DataGridRenderMode;
|
||||
structureEditorDensity: StructureEditorDensity;
|
||||
|
|
@ -311,6 +312,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
|||
redisScanPageSize: 1000,
|
||||
mongoViewMode: "document",
|
||||
showColumnCommentsInHeader: false,
|
||||
showColumnTypesInHeader: true,
|
||||
compactColumnHeaderActions: true,
|
||||
dataGridRenderMode: "canvas",
|
||||
structureEditorDensity: "compact",
|
||||
|
|
@ -466,6 +468,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
|||
mongoViewMode: settings.mongoViewMode === "table" ? "table" : DEFAULT_EDITOR_SETTINGS.mongoViewMode,
|
||||
showColumnCommentsInHeader:
|
||||
settings.showColumnCommentsInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader,
|
||||
showColumnTypesInHeader: settings.showColumnTypesInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader,
|
||||
compactColumnHeaderActions:
|
||||
settings.compactColumnHeaderActions ?? DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions,
|
||||
dataGridRenderMode: normalizeDataGridRenderMode(settings.dataGridRenderMode),
|
||||
|
|
@ -648,6 +651,8 @@ export const useSettingsStore = defineStore("settings", () => {
|
|||
if (partial.mongoViewMode !== undefined) editorSettings.value.mongoViewMode = partial.mongoViewMode;
|
||||
if (partial.showColumnCommentsInHeader !== undefined)
|
||||
editorSettings.value.showColumnCommentsInHeader = partial.showColumnCommentsInHeader;
|
||||
if (partial.showColumnTypesInHeader !== undefined)
|
||||
editorSettings.value.showColumnTypesInHeader = partial.showColumnTypesInHeader;
|
||||
if (partial.compactColumnHeaderActions !== undefined)
|
||||
editorSettings.value.compactColumnHeaderActions = partial.compactColumnHeaderActions;
|
||||
if (partial.dataGridRenderMode !== undefined)
|
||||
|
|
|
|||
|
|
@ -236,6 +236,12 @@ export interface TriggerInfo {
|
|||
|
||||
export interface QueryResult {
|
||||
columns: string[];
|
||||
/**
|
||||
* Database type name for each column, parallel to `columns`. Optional and may
|
||||
* be shorter/empty when a driver cannot supply types (schemaless stores,
|
||||
* fallback query paths, older backends). Consumers must tolerate gaps.
|
||||
*/
|
||||
column_types?: string[];
|
||||
rows: (string | number | boolean | null)[][];
|
||||
affected_rows: number;
|
||||
execution_time_ms: number;
|
||||
|
|
|
|||
|
|
@ -148,13 +148,23 @@ fn query_result_row_limit(max_rows: Option<usize>) -> usize {
|
|||
|
||||
fn limited_query_result(result: ChJsonResult, execution_time_ms: u128, max_rows: Option<usize>) -> QueryResult {
|
||||
let columns: Vec<String> = result.meta.iter().map(|c| c.name.clone()).collect();
|
||||
let column_types: Vec<String> = result.meta.iter().map(|c| c._type.clone()).collect();
|
||||
let mut rows = result.data;
|
||||
let row_limit = query_result_row_limit(max_rows);
|
||||
let truncated = rows.len() > row_limit;
|
||||
if truncated {
|
||||
rows.truncate(row_limit);
|
||||
}
|
||||
QueryResult { columns, rows, affected_rows: 0, execution_time_ms, truncated, session_id: None, has_more: false }
|
||||
QueryResult {
|
||||
columns,
|
||||
column_types,
|
||||
rows,
|
||||
affected_rows: 0,
|
||||
execution_time_ms,
|
||||
truncated,
|
||||
session_id: None,
|
||||
has_more: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_connection(client: &ChClient, timeout: Duration) -> Result<(), String> {
|
||||
|
|
@ -259,6 +269,7 @@ pub async fn execute_query_with_max_rows(
|
|||
}
|
||||
Ok(QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
|
|||
|
|
@ -421,6 +421,7 @@ pub async fn execute_rest_query(client: &EsClient, input: &str) -> Result<crate:
|
|||
|
||||
Ok(crate::types::QueryResult {
|
||||
columns: all_keys,
|
||||
column_types: Vec::new(),
|
||||
rows,
|
||||
affected_rows: total,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -434,6 +435,7 @@ pub async fn execute_rest_query(client: &EsClient, input: &str) -> Result<crate:
|
|||
let row_count = rows.len() as u64;
|
||||
Ok(crate::types::QueryResult {
|
||||
columns,
|
||||
column_types: Vec::new(),
|
||||
rows,
|
||||
affected_rows: row_count,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -445,6 +447,7 @@ pub async fn execute_rest_query(client: &EsClient, input: &str) -> Result<crate:
|
|||
let pretty = serde_json::to_string_pretty(&body).unwrap_or_else(|_| body.to_string());
|
||||
Ok(crate::types::QueryResult {
|
||||
columns: vec!["status".to_string(), "response".to_string()],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![vec![serde_json::Value::Number(status.into()), serde_json::Value::String(pretty)]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -457,6 +460,7 @@ pub async fn execute_rest_query(client: &EsClient, input: &str) -> Result<crate:
|
|||
let pretty = serde_json::to_string_pretty(&body).unwrap_or_else(|_| body.to_string());
|
||||
Ok(crate::types::QueryResult {
|
||||
columns: vec!["status".to_string(), "response".to_string()],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![vec![serde_json::Value::Number(status.into()), serde_json::Value::String(pretty)]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
|
|||
|
|
@ -187,6 +187,42 @@ fn mysql_bytes_to_json(bytes: Vec<u8>, column: &mysql_async::Column) -> serde_js
|
|||
serde_json::Value::String(String::from_utf8_lossy(&bytes).to_string())
|
||||
}
|
||||
|
||||
/// Map a MySQL column to a user-facing type name for the result-grid header.
|
||||
/// Returns the bare lowercase type name (no length/precision/signedness), which
|
||||
/// is enough for display; unknown variants fall back to a lowercased debug name.
|
||||
fn mysql_column_type_name(ty: ColumnType) -> String {
|
||||
use mysql_async::consts::ColumnType::*;
|
||||
match ty {
|
||||
MYSQL_TYPE_TINY => "tinyint",
|
||||
MYSQL_TYPE_SHORT => "smallint",
|
||||
MYSQL_TYPE_INT24 => "mediumint",
|
||||
MYSQL_TYPE_LONG => "int",
|
||||
MYSQL_TYPE_LONGLONG => "bigint",
|
||||
MYSQL_TYPE_FLOAT => "float",
|
||||
MYSQL_TYPE_DOUBLE => "double",
|
||||
MYSQL_TYPE_DECIMAL | MYSQL_TYPE_NEWDECIMAL => "decimal",
|
||||
MYSQL_TYPE_BIT => "bit",
|
||||
MYSQL_TYPE_YEAR => "year",
|
||||
MYSQL_TYPE_DATE | MYSQL_TYPE_NEWDATE => "date",
|
||||
MYSQL_TYPE_TIME | MYSQL_TYPE_TIME2 => "time",
|
||||
MYSQL_TYPE_DATETIME | MYSQL_TYPE_DATETIME2 => "datetime",
|
||||
MYSQL_TYPE_TIMESTAMP | MYSQL_TYPE_TIMESTAMP2 => "timestamp",
|
||||
MYSQL_TYPE_JSON => "json",
|
||||
MYSQL_TYPE_ENUM => "enum",
|
||||
MYSQL_TYPE_SET => "set",
|
||||
MYSQL_TYPE_TINY_BLOB => "tinyblob",
|
||||
MYSQL_TYPE_MEDIUM_BLOB => "mediumblob",
|
||||
MYSQL_TYPE_LONG_BLOB => "longblob",
|
||||
MYSQL_TYPE_BLOB => "blob",
|
||||
MYSQL_TYPE_VARCHAR | MYSQL_TYPE_VAR_STRING => "varchar",
|
||||
MYSQL_TYPE_STRING => "char",
|
||||
MYSQL_TYPE_GEOMETRY => "geometry",
|
||||
MYSQL_TYPE_NULL => "null",
|
||||
other => return format!("{:?}", other).to_lowercase(),
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn mysql_value_to_json(row: &mysql_async::Row, idx: usize) -> serde_json::Value {
|
||||
let Some(column) = row.columns_ref().get(idx) else {
|
||||
return serde_json::Value::Null;
|
||||
|
|
@ -1251,6 +1287,8 @@ async fn execute_result_set_with_text_protocol_on_conn(
|
|||
) -> Result<QueryResult, String> {
|
||||
let mut result = conn.query_iter(sql).await.map_err(|e| e.to_string())?;
|
||||
let columns: Vec<String> = result.columns_ref().iter().map(|c| c.name_str().to_string()).collect();
|
||||
let column_types: Vec<String> =
|
||||
result.columns_ref().iter().map(|c| mysql_column_type_name(c.column_type())).collect();
|
||||
|
||||
let mut result_rows: Vec<Vec<serde_json::Value>> = Vec::new();
|
||||
let mut stream = result
|
||||
|
|
@ -1275,6 +1313,7 @@ async fn execute_result_set_with_text_protocol_on_conn(
|
|||
|
||||
Ok(QueryResult {
|
||||
columns,
|
||||
column_types,
|
||||
rows: result_rows,
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -1292,6 +1331,8 @@ async fn execute_result_set_with_prepared_protocol_on_conn(
|
|||
) -> Result<QueryResult, String> {
|
||||
let mut result = conn.exec_iter(sql, ()).await.map_err(|e| e.to_string())?;
|
||||
let columns: Vec<String> = result.columns_ref().iter().map(|c| c.name_str().to_string()).collect();
|
||||
let column_types: Vec<String> =
|
||||
result.columns_ref().iter().map(|c| mysql_column_type_name(c.column_type())).collect();
|
||||
|
||||
let mut result_rows: Vec<Vec<serde_json::Value>> = Vec::new();
|
||||
let mut stream = result
|
||||
|
|
@ -1316,6 +1357,7 @@ async fn execute_result_set_with_prepared_protocol_on_conn(
|
|||
|
||||
Ok(QueryResult {
|
||||
columns,
|
||||
column_types,
|
||||
rows: result_rows,
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -1378,6 +1420,7 @@ pub async fn execute_query_on_conn_with_max_rows(
|
|||
|
||||
Ok(QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -1566,6 +1609,21 @@ mod tests {
|
|||
use super::*;
|
||||
use mysql_async::consts::ColumnFlags;
|
||||
|
||||
#[test]
|
||||
fn mysql_column_type_names_map_to_friendly_names() {
|
||||
use mysql_async::consts::ColumnType::*;
|
||||
assert_eq!(mysql_column_type_name(MYSQL_TYPE_TINY), "tinyint");
|
||||
assert_eq!(mysql_column_type_name(MYSQL_TYPE_LONG), "int");
|
||||
assert_eq!(mysql_column_type_name(MYSQL_TYPE_LONGLONG), "bigint");
|
||||
assert_eq!(mysql_column_type_name(MYSQL_TYPE_NEWDECIMAL), "decimal");
|
||||
assert_eq!(mysql_column_type_name(MYSQL_TYPE_VARCHAR), "varchar");
|
||||
assert_eq!(mysql_column_type_name(MYSQL_TYPE_VAR_STRING), "varchar");
|
||||
assert_eq!(mysql_column_type_name(MYSQL_TYPE_STRING), "char");
|
||||
assert_eq!(mysql_column_type_name(MYSQL_TYPE_DATETIME), "datetime");
|
||||
assert_eq!(mysql_column_type_name(MYSQL_TYPE_JSON), "json");
|
||||
assert_eq!(mysql_column_type_name(MYSQL_TYPE_BLOB), "blob");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_with_queries_are_treated_as_result_sets() {
|
||||
let sql = "WITH RECURSIVE org_tree AS (SELECT 1 AS id) SELECT id FROM org_tree";
|
||||
|
|
|
|||
|
|
@ -692,6 +692,7 @@ async fn execute_select_prepared(
|
|||
truncated,
|
||||
session_id: None,
|
||||
has_more: false,
|
||||
column_types,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -741,6 +742,7 @@ async fn execute_select_text(
|
|||
truncated,
|
||||
session_id: None,
|
||||
has_more: false,
|
||||
column_types: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1434,6 +1436,7 @@ pub async fn execute_query_with_max_rows(
|
|||
truncated: false,
|
||||
session_id: None,
|
||||
has_more: false,
|
||||
column_types: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1519,6 +1522,7 @@ async fn execute_query_with_max_rows_inner(
|
|||
truncated: false,
|
||||
session_id: None,
|
||||
has_more: false,
|
||||
column_types: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ pub async fn execute_query_with_max_rows(
|
|||
let affected_rows = result.rows_affected.unwrap_or(0);
|
||||
Ok(QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -340,6 +341,7 @@ fn query_result_from_rqlite_result(
|
|||
}
|
||||
QueryResult {
|
||||
columns: result.columns,
|
||||
column_types: Vec::new(),
|
||||
rows: result.values,
|
||||
affected_rows: 0,
|
||||
execution_time_ms,
|
||||
|
|
|
|||
|
|
@ -637,6 +637,7 @@ fn execute_query_blocking(pool: &SqliteHandle, sql: &str, max_rows: Option<usize
|
|||
|
||||
Ok(QueryResult {
|
||||
columns,
|
||||
column_types: Vec::new(),
|
||||
rows: result_rows,
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -648,6 +649,7 @@ fn execute_query_blocking(pool: &SqliteHandle, sql: &str, max_rows: Option<usize
|
|||
conn.execute_batch(sql).map_err(|e| e.to_string())?;
|
||||
Ok(QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows: conn.changes(),
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
|
|||
|
|
@ -98,6 +98,17 @@ fn columns_from_metadata(metadata: &tiberius::ResultMetadata) -> Vec<String> {
|
|||
metadata.columns().iter().map(|c| c.name().to_string()).collect()
|
||||
}
|
||||
|
||||
/// Map a tiberius column to a user-facing type name for the result-grid header.
|
||||
/// Uses the TDS column-type debug name lowercased; good enough for display, with
|
||||
/// no risk of mismatching the enum variants across tiberius versions.
|
||||
fn sqlserver_column_type_name(column: &tiberius::Column) -> String {
|
||||
format!("{:?}", column.column_type()).to_lowercase()
|
||||
}
|
||||
|
||||
fn column_types_from_metadata(metadata: &tiberius::ResultMetadata) -> Vec<String> {
|
||||
metadata.columns().iter().map(sqlserver_column_type_name).collect()
|
||||
}
|
||||
|
||||
async fn collect_first_result_limited(
|
||||
mut stream: QueryStream<'_>,
|
||||
start: Instant,
|
||||
|
|
@ -105,6 +116,7 @@ async fn collect_first_result_limited(
|
|||
) -> Result<QueryResult, String> {
|
||||
let row_limit = query_result_row_limit(max_rows);
|
||||
let mut columns: Vec<String> = vec![];
|
||||
let mut column_types: Vec<String> = vec![];
|
||||
let mut rows: Vec<Vec<serde_json::Value>> = Vec::new();
|
||||
let mut truncated = false;
|
||||
|
||||
|
|
@ -112,6 +124,7 @@ async fn collect_first_result_limited(
|
|||
match item {
|
||||
QueryItem::Metadata(metadata) if metadata.result_index() == 0 => {
|
||||
columns = columns_from_metadata(&metadata);
|
||||
column_types = column_types_from_metadata(&metadata);
|
||||
}
|
||||
QueryItem::Metadata(_) => {}
|
||||
QueryItem::Row(row) if row.result_index() == 0 => {
|
||||
|
|
@ -127,6 +140,7 @@ async fn collect_first_result_limited(
|
|||
|
||||
Ok(QueryResult {
|
||||
columns,
|
||||
column_types,
|
||||
rows,
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -138,6 +152,7 @@ async fn collect_first_result_limited(
|
|||
|
||||
struct SqlServerResultSet {
|
||||
columns: Vec<String>,
|
||||
column_types: Vec<String>,
|
||||
rows: Vec<Vec<serde_json::Value>>,
|
||||
truncated: bool,
|
||||
}
|
||||
|
|
@ -467,6 +482,7 @@ fn push_sqlserver_result_set(results: &mut Vec<QueryResult>, result: Option<SqlS
|
|||
}
|
||||
results.push(QueryResult {
|
||||
columns: result.columns,
|
||||
column_types: result.column_types,
|
||||
rows: result.rows,
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -492,6 +508,7 @@ async fn collect_result_sets_limited(
|
|||
push_sqlserver_result_set(&mut results, current.take(), start);
|
||||
current = Some(SqlServerResultSet {
|
||||
columns: columns_from_metadata(&metadata),
|
||||
column_types: column_types_from_metadata(&metadata),
|
||||
rows: Vec::new(),
|
||||
truncated: false,
|
||||
});
|
||||
|
|
@ -499,6 +516,7 @@ async fn collect_result_sets_limited(
|
|||
QueryItem::Row(row) => {
|
||||
let result = current.get_or_insert_with(|| SqlServerResultSet {
|
||||
columns: row.columns().iter().map(|c| c.name().to_string()).collect(),
|
||||
column_types: row.columns().iter().map(sqlserver_column_type_name).collect(),
|
||||
rows: Vec::new(),
|
||||
truncated: false,
|
||||
});
|
||||
|
|
@ -922,6 +940,7 @@ pub async fn execute_query_with_max_rows(
|
|||
let _ = sqlserver_driver_result(collect_result_sets_limited(stream, start, max_rows)).await?;
|
||||
Ok(QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -933,6 +952,7 @@ pub async fn execute_query_with_max_rows(
|
|||
let result = sqlserver_driver_result(client.execute(sql, &[])).await?;
|
||||
Ok(QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows: result.rows_affected().iter().sum::<u64>(),
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -967,6 +987,7 @@ pub async fn execute_batch_with_max_rows(
|
|||
if results.is_empty() {
|
||||
results.push(QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -1262,6 +1283,7 @@ mod tests {
|
|||
&mut results,
|
||||
Some(SqlServerResultSet {
|
||||
columns: vec!["id".to_string(), "name".to_string()],
|
||||
column_types: vec![],
|
||||
rows: vec![],
|
||||
truncated: false,
|
||||
}),
|
||||
|
|
@ -1278,7 +1300,7 @@ mod tests {
|
|||
let mut results = Vec::new();
|
||||
super::push_sqlserver_result_set(
|
||||
&mut results,
|
||||
Some(SqlServerResultSet { columns: vec![], rows: vec![], truncated: false }),
|
||||
Some(SqlServerResultSet { columns: vec![], column_types: vec![], rows: vec![], truncated: false }),
|
||||
Instant::now(),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@ pub fn duckdb_execute_with_max_rows(
|
|||
}
|
||||
Ok(db::QueryResult {
|
||||
columns,
|
||||
column_types: Vec::new(),
|
||||
rows: result_rows,
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -245,6 +246,7 @@ pub fn duckdb_execute_with_max_rows(
|
|||
let affected = con.execute(sql, []).map_err(|e| e.to_string())?;
|
||||
Ok(db::QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows: affected as u64,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -948,6 +950,7 @@ async fn execute_multi_mysql(
|
|||
fn error_query_result(message: String) -> db::QueryResult {
|
||||
db::QueryResult {
|
||||
columns: vec!["Error".to_string()],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![vec![serde_json::Value::String(message)]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 0,
|
||||
|
|
@ -972,6 +975,7 @@ async fn execute_multi_sqlserver(
|
|||
if is_canceled(&cancel_token) {
|
||||
all_results.push(db::QueryResult {
|
||||
columns: vec!["Error".to_string()],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![vec![serde_json::Value::String(canceled_error())]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 0,
|
||||
|
|
@ -1004,6 +1008,7 @@ async fn execute_multi_sqlserver(
|
|||
Err(e) => {
|
||||
all_results.push(db::QueryResult {
|
||||
columns: vec!["Error".to_string()],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![vec![serde_json::Value::String(e)]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 0,
|
||||
|
|
@ -1018,6 +1023,7 @@ async fn execute_multi_sqlserver(
|
|||
if all_results.is_empty() {
|
||||
all_results.push(db::QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 0,
|
||||
|
|
@ -1081,6 +1087,7 @@ pub async fn execute_statements(
|
|||
|
||||
Ok(db::QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows: total_affected,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -1183,6 +1190,7 @@ async fn exec_tx_pg_inner(
|
|||
match tx_result {
|
||||
Ok(total_affected) => Ok(db::QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows: total_affected,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -1230,6 +1238,7 @@ async fn exec_tx_mysql_inner(
|
|||
conn.query_drop("COMMIT").await.map_err(|e| format!("COMMIT failed: {}", e))?;
|
||||
Ok(db::QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows: total_affected,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -1261,6 +1270,7 @@ async fn exec_tx_sqlite_inner(
|
|||
conn.execute_batch("COMMIT").map_err(|e| format!("COMMIT failed: {}", e))?;
|
||||
Ok(db::QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows: total_affected,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -1340,6 +1350,7 @@ async fn exec_tx_explicit_inner(
|
|||
|
||||
Ok(db::QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows: total_affected,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -1381,6 +1392,7 @@ async fn exec_tx_none_inner(
|
|||
|
||||
Ok(db::QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows: total_affected,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -1404,6 +1416,7 @@ mod tests {
|
|||
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||
Ok(db::QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 0,
|
||||
|
|
@ -1423,6 +1436,7 @@ mod tests {
|
|||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
Ok(db::QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 0,
|
||||
|
|
@ -1721,6 +1735,7 @@ mod tests {
|
|||
fn query_results_convert_unsafe_json_integers_to_strings_for_js() {
|
||||
let result = db::QueryResult {
|
||||
columns: vec!["id".to_string(), "nested".to_string()],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![vec![
|
||||
serde_json::json!(2_041_797_190_226_354_178_i64),
|
||||
serde_json::json!([1, 2_041_797_190_226_354_178_i64]),
|
||||
|
|
|
|||
|
|
@ -1712,6 +1712,7 @@ pub async fn execute_on_pool_with_max_rows(
|
|||
}
|
||||
Ok(db::QueryResult {
|
||||
columns,
|
||||
column_types: Vec::new(),
|
||||
rows: result_rows,
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
@ -1723,6 +1724,7 @@ pub async fn execute_on_pool_with_max_rows(
|
|||
let affected = con.execute(&sql, []).map_err(|e| e.to_string())?;
|
||||
Ok(db::QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
rows: vec![],
|
||||
affected_rows: affected as u64,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
|
|
|
|||
|
|
@ -61,6 +61,11 @@ pub struct ColumnInfo {
|
|||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct QueryResult {
|
||||
pub columns: Vec<String>,
|
||||
/// Database type name for each column, parallel to `columns`. May be empty
|
||||
/// when a driver cannot supply types (e.g. schemaless stores or fallback
|
||||
/// query paths); consumers must tolerate a shorter/empty vector.
|
||||
#[serde(default)]
|
||||
pub column_types: Vec<String>,
|
||||
pub rows: Vec<Vec<serde_json::Value>>,
|
||||
pub affected_rows: u64,
|
||||
pub execution_time_ms: u128,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
import { resolveHeaderColumnType } from "../../apps/desktop/src/lib/dataGridColumnType.ts";
|
||||
|
||||
test("prefers table-metadata type over the result type", () => {
|
||||
const type = resolveHeaderColumnType({
|
||||
tableColumnType: "numeric(20,6)",
|
||||
resultColumnTypes: ["int4"],
|
||||
actualColIdx: 0,
|
||||
});
|
||||
assert.equal(type, "numeric(20,6)");
|
||||
});
|
||||
|
||||
test("falls back to the result type at the column index when no table meta", () => {
|
||||
const type = resolveHeaderColumnType({
|
||||
tableColumnType: undefined,
|
||||
resultColumnTypes: ["oid", "char", "bigint"],
|
||||
actualColIdx: 1,
|
||||
});
|
||||
assert.equal(type, "char");
|
||||
});
|
||||
|
||||
test("uses actualColIdx (by index), not column order assumptions", () => {
|
||||
// The third result column should resolve to the third type, regardless of
|
||||
// any name-based reordering elsewhere.
|
||||
const type = resolveHeaderColumnType({
|
||||
resultColumnTypes: ["a_type", "b_type", "c_type"],
|
||||
actualColIdx: 2,
|
||||
});
|
||||
assert.equal(type, "c_type");
|
||||
});
|
||||
|
||||
test("returns undefined when the result type index is out of range", () => {
|
||||
const type = resolveHeaderColumnType({
|
||||
resultColumnTypes: ["int4"],
|
||||
actualColIdx: 5,
|
||||
});
|
||||
assert.equal(type, undefined);
|
||||
});
|
||||
|
||||
test("returns undefined when neither source has a type", () => {
|
||||
assert.equal(resolveHeaderColumnType({ actualColIdx: 0 }), undefined);
|
||||
assert.equal(resolveHeaderColumnType({ resultColumnTypes: [], actualColIdx: 0 }), undefined);
|
||||
});
|
||||
|
||||
test("treats blank/whitespace types as absent and falls through", () => {
|
||||
const type = resolveHeaderColumnType({
|
||||
tableColumnType: " ",
|
||||
resultColumnTypes: ["text"],
|
||||
actualColIdx: 0,
|
||||
});
|
||||
assert.equal(type, "text");
|
||||
|
||||
assert.equal(
|
||||
resolveHeaderColumnType({ tableColumnType: "", resultColumnTypes: [""], actualColIdx: 0 }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
Loading…
Reference in New Issue