diff --git a/src/components/TableColumnProfiler.test.ts b/src/components/TableColumnProfiler.test.ts
new file mode 100644
index 00000000..f6053032
--- /dev/null
+++ b/src/components/TableColumnProfiler.test.ts
@@ -0,0 +1,110 @@
+import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { describe, expect, it, vi } from 'vitest';
+import type { TableMetadata } from '../gen/iceberg/types.gen';
+import TableColumnProfiler from './TableColumnProfiler.vue';
+
+vi.mock('../plugins/functions', () => ({
+ useFunctions: () => ({
+ copyToClipboard: vi.fn(),
+ listTableColumnTags: vi.fn(),
+ }),
+}));
+
+vi.mock('../composables/useLoQE', () => ({
+ useLoQE: () => ({}),
+}));
+
+vi.mock('../stores/user', () => ({
+ useUserStore: () => ({ user: { access_token: '' } }),
+}));
+
+vi.mock('../stores/visual', () => ({
+ useVisualStore: () => ({ themeLight: true, tagsRefresh: 0 }),
+}));
+
+vi.mock('../stores/loqe', () => ({
+ useLoQEStore: () => ({
+ clearTableProfiles: vi.fn(),
+ getTableProfiles: () => ({}),
+ }),
+}));
+
+describe('TableColumnProfiler', () => {
+ it('shows field documentation from the current schema for top-level and nested fields', async () => {
+ const metadata = {
+ 'current-schema-id': 2,
+ schemas: [
+ {
+ 'schema-id': 1,
+ type: 'struct',
+ fields: [{ id: 1, name: 'legacy', type: 'string', required: false, doc: 'Legacy field' }],
+ },
+ {
+ 'schema-id': 2,
+ type: 'struct',
+ fields: [
+ {
+ id: 2,
+ name: 'customer',
+ required: true,
+ doc: 'Current customer record',
+ type: {
+ type: 'struct',
+ fields: [
+ {
+ id: 3,
+ name: 'email',
+ type: 'string',
+ required: false,
+ doc: 'Primary contact address',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ } as unknown as TableMetadata;
+
+ const wrapper = mount(TableColumnProfiler, {
+ props: { metadata },
+ global: {
+ stubs: {
+ 'v-alert': { template: '
' },
+ 'v-btn': {
+ props: ['icon'],
+ emits: ['click'],
+ template: '',
+ },
+ 'v-btn-toggle': { template: '
' },
+ 'v-card': { template: '
' },
+ 'v-card-text': { template: '
' },
+ 'v-card-title': { template: '
' },
+ 'v-chip': { template: '' },
+ 'v-dialog': { template: '
' },
+ 'v-divider': { template: '' },
+ 'v-icon': { template: '' },
+ 'v-progress-circular': { template: '' },
+ 'v-select': { template: '' },
+ 'v-spacer': { template: '' },
+ 'v-table': { template: '
' },
+ 'v-tooltip': { template: '
' },
+ },
+ },
+ });
+
+ const initialFieldDocs = wrapper.findAll('.field-doc').map((node) => node.text());
+ expect(initialFieldDocs).toContain('Current customer record');
+ expect(initialFieldDocs).not.toContain('Legacy field');
+ expect(initialFieldDocs).not.toContain('Primary contact address');
+
+ await wrapper.get('button[data-icon="mdi-chevron-right"]').trigger('click');
+ await nextTick();
+
+ const expandedFieldDocs = wrapper.findAll('.field-doc').map((node) => node.text());
+ expect(expandedFieldDocs).toContain('Current customer record');
+ expect(expandedFieldDocs).not.toContain('Legacy field');
+ expect(expandedFieldDocs).toContain('Primary contact address');
+ });
+});
diff --git a/src/components/TableColumnProfiler.vue b/src/components/TableColumnProfiler.vue
index 9e7b25cd..d04fb78a 100644
--- a/src/components/TableColumnProfiler.vue
+++ b/src/components/TableColumnProfiler.vue
@@ -186,6 +186,9 @@
{{ row.name }}
+
+ {{ row.doc }}
+
makeNode(f.name, f.type, parentKey, depth));
+ return (t.fields ?? []).map((f: StructField) =>
+ makeNode(f.name, f.type, f.doc, parentKey, depth),
+ );
}
if (t.type === 'list') {
const el = t.element;
if (el && typeof el === 'object') {
if (el.type === 'struct') return childrenOf(el, parentKey, depth);
- return [makeNode('element', el, parentKey, depth)];
+ return [makeNode('element', el, undefined, parentKey, depth)];
}
return [];
}
if (t.type === 'map') {
- return [makeNode('key', t.key, parentKey, depth), makeNode('value', t.value, parentKey, depth)];
+ return [
+ makeNode('key', t.key, undefined, parentKey, depth),
+ makeNode('value', t.value, undefined, parentKey, depth),
+ ];
}
return [];
}
-function makeNode(name: string, type: any, parentKey: string, parentDepth: number): SchemaNode {
+function makeNode(
+ name: string,
+ type: any,
+ doc: string | undefined,
+ parentKey: string,
+ parentDepth: number,
+): SchemaNode {
const depth = parentDepth + 1;
const key = `${parentKey}.${name}`;
const children = childrenOf(type, key, depth);
return {
key,
name,
+ doc,
type: shortType(type),
depth,
children,
@@ -680,6 +696,7 @@ const schemaTree = computed(() => {
return {
key: f.name,
name: f.name,
+ doc: f.doc,
type: shortType(f.type),
depth: 0,
children,
@@ -951,6 +968,10 @@ watch(
min-width: 220px;
white-space: normal;
}
+.field-doc {
+ overflow-wrap: anywhere;
+ white-space: normal;
+}
.profiler-table :deep(.nested-row) td {
border-bottom: none;
}