Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Visualize complex Hive column types #1072

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions querybook/webapp/__tests__/lib/utils/complex-types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { parseType } from 'lib/utils/complex-types';

test('parseType', () => {
expect(
parseType(
'struct<date:struct<year:int,month:int,day:int>,hour:int,minute:int,second:int,timeZoneId:string>'
)
).toEqual({
date: {
year: 'int',
month: 'int',
day: 'int',
},
hour: 'int',
minute: 'int',
second: 'int',
timeZoneId: 'string',
});

expect(
parseType(
'array<struct<size:struct<width:int,height:int,isAspectRatio:boolean>>>'
)
).toEqual([
{
size: {
width: 'int',
height: 'int',
isAspectRatio: 'boolean',
},
},
]);

expect(
parseType(
'struct<purchasePath:string,resultToken:string,sessionId:string,site:struct<eapid:bigint,tpid:bigint>,tests:array<struct<bucketValue:string,experimentId:string,instanceId:string>>,user:struct<guid:string,tuid:string>>'
)
).toEqual({
purchasePath: 'string',
resultToken: 'string',
sessionId: 'string',
site: {
eapid: 'bigint',
tpid: 'bigint',
},
tests: [
{
bucketValue: 'string',
experimentId: 'string',
instanceId: 'string',
},
],
user: {
guid: 'string',
tuid: 'string',
},
});

expect(parseType('map<string,string>')).toEqual({
key: 'string',
value: 'string',
});

expect(
parseType(
'map<string,uniontype<string,int,bigint,float,double,struct<year:int,month:int,day:int>>>'
)
).toEqual({
key: 'string',
value: [
'string',
'int',
'bigint',
'float',
'double',
{ year: 'int', month: 'int', day: 'int' },
],
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import * as React from 'react';

import { DataTableColumnStats } from 'components/DataTableStats/DataTableColumnStats';
import { IDataColumn } from 'const/metastore';
import { parseType } from 'lib/utils/complex-types';
import { Card } from 'ui/Card/Card';
import { EditableTextField } from 'ui/EditableTextField/EditableTextField';
import { Icon } from 'ui/Icon/Icon';
import { Json } from 'ui/Json/Json';
import { KeyContentDisplay } from 'ui/KeyContentDisplay/KeyContentDisplay';
import { AccentText, StyledText } from 'ui/StyledText/StyledText';

Expand All @@ -31,6 +33,9 @@ export const DataTableColumnCard: React.FunctionComponent<IProps> = ({
onSave={updateDataColumnDescription.bind(null, column.id)}
/>
);

const parsedType = parseType(column.type);

return (
<div className="DataTableColumnCard">
<Card key={column.id} alignLeft>
Expand All @@ -57,6 +62,13 @@ export const DataTableColumnCard: React.FunctionComponent<IProps> = ({
{column.comment}
</KeyContentDisplay>
)}
{parsedType !== column.type && (
<>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why need the <> </>? it only has a single child

<KeyContentDisplay keyString="Complex Type">
<Json json={parsedType} />
</KeyContentDisplay>
</>
)}
<KeyContentDisplay keyString="User Comments">
{userCommentsContent}
</KeyContentDisplay>
Expand Down
12 changes: 12 additions & 0 deletions querybook/webapp/components/DataTableViewMini/ColumnPanelView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { ContentState } from 'draft-js';
import React from 'react';
import { useSelector } from 'react-redux';

import { parseType } from 'lib/utils/complex-types';
import { IStoreState } from 'redux/store/types';
import { Json } from 'ui/Json/Json';

import { PanelSection, SubPanelSection } from './PanelSection';

Expand All @@ -19,6 +21,8 @@ export const ColumnPanelView: React.FunctionComponent<
(state: IStoreState) => state.dataSources.dataColumnsById[columnId]
);

const parsedType = parseType(column.type);

const overviewPanel = (
<PanelSection title="column">
<SubPanelSection title="name">{column.name}</SubPanelSection>
Expand All @@ -39,11 +43,19 @@ export const ColumnPanelView: React.FunctionComponent<
<PanelSection title="type info">{typeInfo}</PanelSection>
) : null;

const complexTypePanel =
parsedType !== column.type ? (
<PanelSection title="complex type">
<Json json={parsedType} />
</PanelSection>
) : null;

return (
<>
{overviewPanel}
{descriptionPanel}
{typeInfoPanel}
{complexTypePanel}
</>
);
};
33 changes: 2 additions & 31 deletions querybook/webapp/lib/query-result/transformer.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,19 @@
import JSONBig from 'json-bigint';
import React from 'react';
import ReactJson, { ThemeObject } from 'react-json-view';

import {
formatNumber,
getHumanReadableNumber,
isNumeric,
roundNumberToDecimal,
} from 'lib/utils/number';
import { Json } from 'ui/Json/Json';
import { Link } from 'ui/Link/Link';

import { IColumnTransformer } from './types';

const JSONBigString = JSONBig({ storeAsString: true });

const ReactJsonTheme: ThemeObject = {
base00: 'transparent', // background
base01: 'var(--text)', // not used
base02: 'var(--color-accent-dark)', // vertical lines, null value frame
base03: 'var(--text)', // not used
base04: 'var(--text-light)', // number of items
base05: 'var(--text)', // not used
base06: 'var(--text)', // not used
base07: 'var(--color-accent-dark)', // struct keys, curly brackets, colon
base08: 'var(--text)', // not used
base09: 'var(--text)', // string value, ellipsis
base0A: 'var(--color-accent-lightest)', // null value text
base0B: 'var(--text)', // not used
base0C: 'var(--color-accent-dark)', // array indices
base0D: 'var(--color-accent-dark)', // collapse
base0E: 'var(--color-accent-dark)', // expand
base0F: 'var(--text)', // int value
};

const queryResultTransformers: IColumnTransformer[] = [
{
key: 'with-comma',
Expand Down Expand Up @@ -104,17 +85,7 @@ const queryResultTransformers: IColumnTransformer[] = [
if (!json || typeof json !== 'object') {
return v;
}
return (
<ReactJson
collapsed={1} // keep first level expanded by default
displayDataTypes={false}
enableClipboard={false}
name={false}
quotesOnKeys={false}
src={json}
theme={ReactJsonTheme}
/>
);
return <Json json={json} />;
} catch (e) {
console.error(e);
return v;
Expand Down
153 changes: 153 additions & 0 deletions querybook/webapp/lib/utils/complex-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* Convert a complex Hive type string to a nested JSON object
*
* Example: 'struct<date:struct<year:int,month:int,day:int>,hour:int,minute:int,second:int,timeZoneId:string>'
* Output: {
date: {
year: 'int',
month: 'int',
day: 'int',
},
hour: 'int',
minute: 'int',
second: 'int',
timeZoneId: 'string',
}
*/
export function parseType(type: string): any | string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will there be cases of

  • uppercase, e.g. STRUCT
  • space between, e.g. struct <

and what happens if the parsing fails?

if (type.startsWith('struct<')) {
return parseStructType(type);
} else if (type.startsWith('array<')) {
return parseArrayType(type);
} else if (type.startsWith('map<')) {
return parseMapType(type);
} else if (type.startsWith('uniontype<')) {
return parseUnionType(type);
} else {
return type;
}
}

export function parseStructType(type: string): Record<string, any> | string {
const structRegex = /struct<(.*)>/;
const structMatch = type.match(structRegex);
if (!structMatch) {
return type;
}

const structStr = structMatch[1];
const structObj: Record<string, any> = {};
let currentKey = '';
let currentVal = '';
let depth = 0;

for (const char of structStr) {
if (char === ':') {
if (depth > 0) {
currentVal += char;
} else {
currentKey = currentVal;
currentVal = '';
}
} else if (char === ',') {
if (depth === 0) {
structObj[currentKey] = parseType(currentVal);
currentKey = '';
currentVal = '';
} else {
currentVal += char;
}
} else if (char === '<') {
depth += 1;
currentVal += char;
} else if (char === '>') {
depth -= 1;
currentVal += char;
} else {
currentVal += char;
}
}

structObj[currentKey] = parseType(currentVal);
return structObj;
}

export function parseMapType(type: string): Record<string, any> | string {
const mapRegex = /map<(.*)>/;
const mapMatch = type.match(mapRegex);
if (!mapMatch) {
return type;
}

const mapStr = mapMatch[1];
const mapObj: Record<string, any> = {};
let currentKey = '';
let currentVal = '';
let depth = 0;

for (const char of mapStr) {
if (char === ',') {
if (depth > 0) {
currentVal += char;
} else {
currentKey = currentVal;
currentVal = '';
}
} else if (char === '<') {
depth += 1;
currentVal += char;
} else if (char === '>') {
depth -= 1;
currentVal += char;
} else {
currentVal += char;
}
}

mapObj.key = parseType(currentKey);
mapObj.value = parseType(currentVal);
return mapObj;
}

export function parseUnionType(type: string): Record<string, any> | string {
const unionRegex = /uniontype<(.*)>/;
const unionMatch = type.match(unionRegex);
if (!unionMatch) {
return type;
}

const unionStr = unionMatch[1];
const unionList: string[] = [];
let currentVal = '';
let depth = 0;

for (const char of unionStr) {
if (char === ',') {
if (depth > 0) {
currentVal += char;
} else {
unionList.push(currentVal);
currentVal = '';
}
} else if (char === '<') {
depth += 1;
currentVal += char;
} else if (char === '>') {
depth -= 1;
currentVal += char;
} else {
currentVal += char;
}
}
unionList.push(currentVal);

return unionList.map((t) => parseType(t));
}

export function parseArrayType(type: string): Record<string, any> | string {
if (type.startsWith('array<')) {
const array = type.substring('array<'.length, type.length - 1);
return [parseType(array)];
}
return type;
}
Loading