Skip to content

Commit c12d450

Browse files
AndrewChubatiukAndrew Chubatiukrestyled-commitsAndrii Chubatiuk
authored
show pg and athena column comments and table descriptions as antd tooltip if they are defined (#6582)
* show column comments by default for athena and postgres * Restyled by prettier * fixed typo * fmt fix * ordered imports * fixed unit tests * fixed tests for athena --------- Co-authored-by: Andrew Chubatiuk <andrew.chubatiuk@motional.com> Co-authored-by: Restyled.io <commits@restyled.io> Co-authored-by: Andrii Chubatiuk <wachy@Andriis-MBP-2.lan>
1 parent 6d64127 commit c12d450

File tree

6 files changed

+142
-43
lines changed

6 files changed

+142
-43
lines changed

client/app/components/queries/SchemaBrowser.jsx

+52-22
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import LoadingState from "../items-list/components/LoadingState";
1616
const SchemaItemColumnType = PropTypes.shape({
1717
name: PropTypes.string.isRequired,
1818
type: PropTypes.string,
19+
comment: PropTypes.string,
1920
});
2021

2122
export const SchemaItemType = PropTypes.shape({
@@ -47,13 +48,30 @@ function SchemaItem({ item, expanded, onToggle, onSelect, ...props }) {
4748
return (
4849
<div {...props}>
4950
<div className="schema-list-item">
50-
<PlainButton className="table-name" onClick={onToggle}>
51-
<i className="fa fa-table m-r-5" aria-hidden="true" />
52-
<strong>
53-
<span title={item.name}>{tableDisplayName}</span>
54-
{!isNil(item.size) && <span> ({item.size})</span>}
55-
</strong>
56-
</PlainButton>
51+
{item.description ? (
52+
<Tooltip
53+
title={item.description}
54+
mouseEnterDelay={0}
55+
mouseLeaveDelay={0}
56+
placement="right"
57+
arrowPointAtCenter>
58+
<PlainButton className="table-name" onClick={onToggle}>
59+
<i className="fa fa-table m-r-5" aria-hidden="true" />
60+
<strong>
61+
<span title={item.name}>{tableDisplayName}</span>
62+
{!isNil(item.size) && <span> ({item.size})</span>}
63+
</strong>
64+
</PlainButton>
65+
</Tooltip>
66+
) : (
67+
<PlainButton className="table-name" onClick={onToggle}>
68+
<i className="fa fa-table m-r-5" aria-hidden="true" />
69+
<strong>
70+
<span title={item.name}>{tableDisplayName}</span>
71+
{!isNil(item.size) && <span> ({item.size})</span>}
72+
</strong>
73+
</PlainButton>
74+
)}
5775
<Tooltip
5876
title="Insert table name into query text"
5977
mouseEnterDelay={0}
@@ -73,22 +91,34 @@ function SchemaItem({ item, expanded, onToggle, onSelect, ...props }) {
7391
map(item.columns, column => {
7492
const columnName = get(column, "name");
7593
const columnType = get(column, "type");
76-
return (
77-
<Tooltip
78-
title="Insert column name into query text"
79-
mouseEnterDelay={0}
80-
mouseLeaveDelay={0}
81-
placement="rightTop">
82-
<PlainButton key={columnName} className="table-open-item" onClick={e => handleSelect(e, columnName)}>
83-
<div>
84-
{columnName} {columnType && <span className="column-type">{columnType}</span>}
85-
</div>
94+
const columnComment = get(column, "comment");
95+
if (columnComment) {
96+
return (
97+
<Tooltip title={columnComment} mouseEnterDelay={0} mouseLeaveDelay={0} placement="rightTop">
98+
<PlainButton
99+
key={columnName}
100+
className="table-open-item"
101+
onClick={e => handleSelect(e, columnName)}>
102+
<div>
103+
{columnName} {columnType && <span className="column-type">{columnType}</span>}
104+
</div>
86105

87-
<div className="copy-to-editor">
88-
<i className="fa fa-angle-double-right" aria-hidden="true" />
89-
</div>
90-
</PlainButton>
91-
</Tooltip>
106+
<div className="copy-to-editor">
107+
<i className="fa fa-angle-double-right" aria-hidden="true" />
108+
</div>
109+
</PlainButton>
110+
</Tooltip>
111+
);
112+
}
113+
return (
114+
<PlainButton key={columnName} className="table-open-item" onClick={e => handleSelect(e, columnName)}>
115+
<div>
116+
{columnName} {columnType && <span className="column-type">{columnType}</span>}
117+
</div>
118+
<div className="copy-to-editor">
119+
<i className="fa fa-angle-double-right" aria-hidden="true" />
120+
</div>
121+
</PlainButton>
92122
);
93123
})
94124
)}

redash/models/__init__.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,16 @@ def get_schema(self, refresh=False):
227227

228228
def _sort_schema(self, schema):
229229
return [
230-
{"name": i["name"], "columns": sorted(i["columns"], key=lambda x: x["name"] if isinstance(x, dict) else x)}
230+
{
231+
"name": i["name"],
232+
"description": i.get("description"),
233+
"columns": sorted(
234+
i["columns"],
235+
key=lambda col: (
236+
("partition" in col["type"], col.get("idx", 0), col["name"]) if isinstance(col, dict) else col
237+
),
238+
),
239+
}
231240
for i in sorted(schema, key=lambda x: x["name"])
232241
]
233242

redash/query_runner/athena.py

+34-5
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121

2222
try:
2323
import boto3
24+
import pandas as pd
2425
import pyathena
26+
from pyathena.pandas_cursor import PandasCursor
2527

2628
enabled = True
2729
except ImportError:
@@ -188,10 +190,35 @@ def __get_schema_from_glue(self):
188190
logger.warning("Glue table doesn't have StorageDescriptor: %s", table_name)
189191
continue
190192
if table_name not in schema:
191-
column = [columns["Name"] for columns in table["StorageDescriptor"]["Columns"]]
192-
schema[table_name] = {"name": table_name, "columns": column}
193-
for partition in table.get("PartitionKeys", []):
194-
schema[table_name]["columns"].append(partition["Name"])
193+
columns = []
194+
for cols in table["StorageDescriptor"]["Columns"]:
195+
c = {
196+
"name": cols["Name"],
197+
}
198+
if "Type" in cols:
199+
c["type"] = cols["Type"]
200+
if "Comment" in cols:
201+
c["comment"] = cols["Comment"]
202+
columns.append(c)
203+
204+
schema[table_name] = {
205+
"name": table_name,
206+
"columns": columns,
207+
"description": table.get("Description"),
208+
}
209+
for idx, partition in enumerate(table.get("PartitionKeys", [])):
210+
schema[table_name]["columns"].append(
211+
{
212+
"name": partition["Name"],
213+
"type": "partition",
214+
"idx": idx,
215+
}
216+
)
217+
if "Type" in partition:
218+
_type = partition["Type"]
219+
c["type"] = f"partition ({_type})"
220+
if "Comment" in partition:
221+
c["comment"] = partition["Comment"]
195222
return list(schema.values())
196223

197224
def get_schema(self, get_stats=False):
@@ -225,14 +252,16 @@ def run_query(self, query, user):
225252
kms_key=self.configuration.get("kms_key", None),
226253
work_group=self.configuration.get("work_group", "primary"),
227254
formatter=SimpleFormatter(),
255+
cursor_class=PandasCursor,
228256
**self._get_iam_credentials(user=user),
229257
).cursor()
230258

231259
try:
232260
cursor.execute(query)
233261
column_tuples = [(i[0], _TYPE_MAPPINGS.get(i[1], None)) for i in cursor.description]
234262
columns = self.fetch_columns(column_tuples)
235-
rows = [dict(zip(([c["name"] for c in columns]), r)) for i, r in enumerate(cursor.fetchall())]
263+
df = cursor.as_pandas().replace({pd.NA: None})
264+
rows = df.to_dict(orient="records")
236265
qbytes = None
237266
athena_query_id = None
238267
try:

redash/query_runner/pg.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ def build_schema(query_result, schema):
108108
column = row["column_name"]
109109
if row.get("data_type") is not None:
110110
column = {"name": row["column_name"], "type": row["data_type"]}
111+
if "column_comment" in row:
112+
column["comment"] = row["column_comment"]
111113

112114
schema[table_name]["columns"].append(column)
113115

@@ -222,7 +224,9 @@ def _get_tables(self, schema):
222224
SELECT s.nspname as table_schema,
223225
c.relname as table_name,
224226
a.attname as column_name,
225-
null as data_type
227+
null as data_type,
228+
null as column_comment,
229+
null as idx
226230
FROM pg_class c
227231
JOIN pg_namespace s
228232
ON c.relnamespace = s.oid
@@ -238,8 +242,16 @@ def _get_tables(self, schema):
238242
SELECT table_schema,
239243
table_name,
240244
column_name,
241-
data_type
242-
FROM information_schema.columns
245+
data_type,
246+
pgd.description,
247+
isc.ordinal_position
248+
FROM information_schema.columns as isc
249+
LEFT JOIN pg_catalog.pg_statio_all_tables as st
250+
ON isc.table_schema = st.schemaname
251+
AND isc.table_name = st.relname
252+
LEFT JOIN pg_catalog.pg_description pgd
253+
ON pgd.objoid=st.relid
254+
AND pgd.objsubid=isc.ordinal_position
243255
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
244256
"""
245257

tests/models/test_data_sources.py

+12-8
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
class DataSourceTest(BaseTestCase):
1010
def test_get_schema(self):
11-
return_value = [{"name": "table", "columns": []}]
11+
return_value = [{"name": "table", "columns": [], "description": None}]
1212

1313
with mock.patch("redash.query_runner.pg.PostgreSQL.get_schema") as patched_get_schema:
1414
patched_get_schema.return_value = return_value
@@ -18,7 +18,7 @@ def test_get_schema(self):
1818
self.assertEqual(return_value, schema)
1919

2020
def test_get_schema_uses_cache(self):
21-
return_value = [{"name": "table", "columns": []}]
21+
return_value = [{"name": "table", "columns": [], "description": None}]
2222
with mock.patch("redash.query_runner.pg.PostgreSQL.get_schema") as patched_get_schema:
2323
patched_get_schema.return_value = return_value
2424

@@ -29,12 +29,12 @@ def test_get_schema_uses_cache(self):
2929
self.assertEqual(patched_get_schema.call_count, 1)
3030

3131
def test_get_schema_skips_cache_with_refresh_true(self):
32-
return_value = [{"name": "table", "columns": []}]
32+
return_value = [{"name": "table", "columns": [], "description": None}]
3333
with mock.patch("redash.query_runner.pg.PostgreSQL.get_schema") as patched_get_schema:
3434
patched_get_schema.return_value = return_value
3535

3636
self.factory.data_source.get_schema()
37-
new_return_value = [{"name": "new_table", "columns": []}]
37+
new_return_value = [{"name": "new_table", "columns": [], "description": None}]
3838
patched_get_schema.return_value = new_return_value
3939
schema = self.factory.data_source.get_schema(refresh=True)
4040

@@ -43,19 +43,21 @@ def test_get_schema_skips_cache_with_refresh_true(self):
4343

4444
def test_schema_sorter(self):
4545
input_data = [
46-
{"name": "zoo", "columns": ["is_zebra", "is_snake", "is_cow"]},
46+
{"name": "zoo", "columns": ["is_zebra", "is_snake", "is_cow"], "description": None},
4747
{
4848
"name": "all_terain_vehicle",
4949
"columns": ["has_wheels", "has_engine", "has_all_wheel_drive"],
50+
"description": None,
5051
},
5152
]
5253

5354
expected_output = [
5455
{
5556
"name": "all_terain_vehicle",
5657
"columns": ["has_all_wheel_drive", "has_engine", "has_wheels"],
58+
"description": None,
5759
},
58-
{"name": "zoo", "columns": ["is_cow", "is_snake", "is_zebra"]},
60+
{"name": "zoo", "columns": ["is_cow", "is_snake", "is_zebra"], "description": None},
5961
]
6062

6163
real_output = self.factory.data_source._sort_schema(input_data)
@@ -64,19 +66,21 @@ def test_schema_sorter(self):
6466

6567
def test_model_uses_schema_sorter(self):
6668
orig_schema = [
67-
{"name": "zoo", "columns": ["is_zebra", "is_snake", "is_cow"]},
69+
{"name": "zoo", "columns": ["is_zebra", "is_snake", "is_cow"], "description": None},
6870
{
6971
"name": "all_terain_vehicle",
7072
"columns": ["has_wheels", "has_engine", "has_all_wheel_drive"],
73+
"description": None,
7174
},
7275
]
7376

7477
sorted_schema = [
7578
{
7679
"name": "all_terain_vehicle",
7780
"columns": ["has_all_wheel_drive", "has_engine", "has_wheels"],
81+
"description": None,
7882
},
79-
{"name": "zoo", "columns": ["is_cow", "is_snake", "is_zebra"]},
83+
{"name": "zoo", "columns": ["is_cow", "is_snake", "is_zebra"], "description": None},
8084
]
8185

8286
with mock.patch("redash.query_runner.pg.PostgreSQL.get_schema") as patched_get_schema:

tests/query_runner/test_athena.py

+19-4
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ def test_external_table(self):
7575
{"DatabaseName": "test1"},
7676
)
7777
with self.stubber:
78-
assert query_runner.get_schema() == [{"columns": ["row_id"], "name": "test1.jdbc_table"}]
78+
assert query_runner.get_schema() == [
79+
{"columns": [{"name": "row_id", "type": "int"}], "name": "test1.jdbc_table", "description": None}
80+
]
7981

8082
def test_partitioned_table(self):
8183
"""
@@ -124,7 +126,16 @@ def test_partitioned_table(self):
124126
{"DatabaseName": "test1"},
125127
)
126128
with self.stubber:
127-
assert query_runner.get_schema() == [{"columns": ["sk", "category"], "name": "test1.partitioned_table"}]
129+
assert query_runner.get_schema() == [
130+
{
131+
"columns": [
132+
{"name": "sk", "type": "partition (int)"},
133+
{"name": "category", "type": "partition", "idx": 0},
134+
],
135+
"name": "test1.partitioned_table",
136+
"description": None,
137+
}
138+
]
128139

129140
def test_view(self):
130141
query_runner = Athena({"glue": True, "region": "mars-east-1"})
@@ -156,7 +167,9 @@ def test_view(self):
156167
{"DatabaseName": "test1"},
157168
)
158169
with self.stubber:
159-
assert query_runner.get_schema() == [{"columns": ["sk"], "name": "test1.view"}]
170+
assert query_runner.get_schema() == [
171+
{"columns": [{"name": "sk", "type": "int"}], "name": "test1.view", "description": None}
172+
]
160173

161174
def test_dodgy_table_does_not_break_schema_listing(self):
162175
"""
@@ -196,7 +209,9 @@ def test_dodgy_table_does_not_break_schema_listing(self):
196209
{"DatabaseName": "test1"},
197210
)
198211
with self.stubber:
199-
assert query_runner.get_schema() == [{"columns": ["region"], "name": "test1.csv"}]
212+
assert query_runner.get_schema() == [
213+
{"columns": [{"name": "region", "type": "string"}], "name": "test1.csv", "description": None}
214+
]
200215

201216
def test_no_storage_descriptor_table(self):
202217
"""

0 commit comments

Comments
 (0)