perspective-python 4.2.0__cp311-abi3-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- perspective/__init__.py +396 -0
- perspective/extension/finos-perspective-nbextension.json +5 -0
- perspective/handlers/__init__.py +11 -0
- perspective/handlers/aiohttp.py +61 -0
- perspective/handlers/starlette.py +55 -0
- perspective/handlers/tornado.py +184 -0
- perspective/perspective.pyd +0 -0
- perspective/templates/exported_widget.html.template +35 -0
- perspective/tests/__init__.py +11 -0
- perspective/tests/async/test_async_client.py +83 -0
- perspective/tests/async/test_websocket_client.py +124 -0
- perspective/tests/conftest.py +272 -0
- perspective/tests/core/__init__.py +11 -0
- perspective/tests/core/test_async.py +351 -0
- perspective/tests/multi_threaded/__init__.py +11 -0
- perspective/tests/multi_threaded/test_multi_threaded.py +201 -0
- perspective/tests/server/__init__.py +11 -0
- perspective/tests/server/test_server.py +1016 -0
- perspective/tests/server/test_session.py +110 -0
- perspective/tests/table/__init__.py +11 -0
- perspective/tests/table/arrow/date32.arrow +0 -0
- perspective/tests/table/arrow/date64.arrow +0 -0
- perspective/tests/table/arrow/dict.arrow +0 -0
- perspective/tests/table/arrow/dict_update.arrow +0 -0
- perspective/tests/table/arrow/int_float_str.arrow +0 -0
- perspective/tests/table/arrow/int_float_str_file.arrow +0 -0
- perspective/tests/table/arrow/int_float_str_update.arrow +0 -0
- perspective/tests/table/object_sequence.py +402 -0
- perspective/tests/table/test_column_paths.py +89 -0
- perspective/tests/table/test_delete.py +124 -0
- perspective/tests/table/test_exception.py +65 -0
- perspective/tests/table/test_leaks.py +54 -0
- perspective/tests/table/test_ports.py +178 -0
- perspective/tests/table/test_remove.py +102 -0
- perspective/tests/table/test_table.py +641 -0
- perspective/tests/table/test_table_arrow.py +503 -0
- perspective/tests/table/test_table_datetime.py +2409 -0
- perspective/tests/table/test_table_infer.py +201 -0
- perspective/tests/table/test_table_limit.py +45 -0
- perspective/tests/table/test_table_numpy.py +1022 -0
- perspective/tests/table/test_table_pandas.py +1018 -0
- perspective/tests/table/test_table_polars.py +251 -0
- perspective/tests/table/test_table_view_table.py +130 -0
- perspective/tests/table/test_to_arrow.py +417 -0
- perspective/tests/table/test_to_arrow_lz4.py +32 -0
- perspective/tests/table/test_to_format.py +1024 -0
- perspective/tests/table/test_to_polars.py +26 -0
- perspective/tests/table/test_update.py +545 -0
- perspective/tests/table/test_update_arrow.py +980 -0
- perspective/tests/table/test_update_pandas.py +211 -0
- perspective/tests/table/test_view.py +2261 -0
- perspective/tests/table/test_view_expression.py +1940 -0
- perspective/tests/test_dependencies.py +53 -0
- perspective/tests/viewer/__init__.py +11 -0
- perspective/tests/viewer/test_viewer.py +246 -0
- perspective/tests/widget/__init__.py +11 -0
- perspective/tests/widget/test_widget.py +278 -0
- perspective/tests/widget/test_widget_pandas.py +453 -0
- perspective/virtual_servers/__init__.py +134 -0
- perspective/virtual_servers/clickhouse.py +245 -0
- perspective/virtual_servers/duckdb.py +236 -0
- perspective/widget/__init__.py +349 -0
- perspective/widget/viewer/__init__.py +15 -0
- perspective/widget/viewer/validate.py +22 -0
- perspective/widget/viewer/viewer.py +343 -0
- perspective/widget/viewer/viewer_traitlets.py +101 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/install.json +5 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/package.json +71 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/253.5f5c9e80605aa4106a28.js +2 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/253.5f5c9e80605aa4106a28.js.LICENSE.txt +25 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/523.c030af5d3c4f67ff83f6.js +1 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/remoteEntry.95a8ea1b44d96032833f.js +1 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/style.js +4 -0
- perspective_python-4.2.0.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/static/third-party-licenses.json +16 -0
- perspective_python-4.2.0.dist-info/METADATA +27 -0
- perspective_python-4.2.0.dist-info/RECORD +79 -0
- perspective_python-4.2.0.dist-info/WHEEL +4 -0
- perspective_python-4.2.0.dist-info/licenses/LICENSE.md +193 -0
- perspective_python-4.2.0.dist-info/licenses/LICENSE_THIRDPARTY_cargo.yml +17395 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
# ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
import perspective
|
|
14
|
+
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from loguru import logger
|
|
17
|
+
|
|
18
|
+
from perspective.virtual_servers import VirtualSessionModel
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
NUMBER_AGGS = [
|
|
22
|
+
"sum",
|
|
23
|
+
"count",
|
|
24
|
+
"any_value",
|
|
25
|
+
"arbitrary",
|
|
26
|
+
"array_agg",
|
|
27
|
+
"avg",
|
|
28
|
+
"bit_and",
|
|
29
|
+
"bit_or",
|
|
30
|
+
"bit_xor",
|
|
31
|
+
"bitstring_agg",
|
|
32
|
+
"bool_and",
|
|
33
|
+
"bool_or",
|
|
34
|
+
"countif",
|
|
35
|
+
"favg",
|
|
36
|
+
"fsum",
|
|
37
|
+
"geomean",
|
|
38
|
+
"kahan_sum",
|
|
39
|
+
"last",
|
|
40
|
+
"max",
|
|
41
|
+
"min",
|
|
42
|
+
"product",
|
|
43
|
+
"string_agg",
|
|
44
|
+
"sumkahan",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
STRING_AGGS = [
|
|
48
|
+
"count",
|
|
49
|
+
"any_value",
|
|
50
|
+
"arbitrary",
|
|
51
|
+
"first",
|
|
52
|
+
"countif",
|
|
53
|
+
"last",
|
|
54
|
+
"string_agg",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
FILTER_OPS = [
|
|
58
|
+
"==",
|
|
59
|
+
"!=",
|
|
60
|
+
"LIKE",
|
|
61
|
+
"IS DISTINCT FROM",
|
|
62
|
+
"IS NOT DISTINCT FROM",
|
|
63
|
+
">=",
|
|
64
|
+
"<=",
|
|
65
|
+
">",
|
|
66
|
+
"<",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ClickhouseVirtualSession:
|
|
71
|
+
def __init__(self, callback, db):
|
|
72
|
+
self.session = perspective.VirtualServer(ClickhouseVirtualSessionModel(db))
|
|
73
|
+
self.callback = callback
|
|
74
|
+
|
|
75
|
+
def handle_request(self, msg):
|
|
76
|
+
self.callback(self.session.handle_request(msg))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ClickhouseVirtualServer:
|
|
80
|
+
def __init__(self, db):
|
|
81
|
+
self.db = db
|
|
82
|
+
|
|
83
|
+
def new_session(self, callback):
|
|
84
|
+
return ClickhouseVirtualSession(callback, self.db)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ClickhouseVirtualSessionModel(VirtualSessionModel):
|
|
88
|
+
"""
|
|
89
|
+
An implementation of a `perspective.VirtualSessionModel` for ClickHouse.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, db):
|
|
93
|
+
self.db = db
|
|
94
|
+
self.sql_builder = perspective.GenericSQLVirtualServerModel(
|
|
95
|
+
{"create_entity": "VIEW", "grouping_fn": "GROUPING"}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def get_features(self):
|
|
99
|
+
return {
|
|
100
|
+
"group_by": True,
|
|
101
|
+
"split_by": False,
|
|
102
|
+
"sort": True,
|
|
103
|
+
"expressions": True,
|
|
104
|
+
"filter_ops": {
|
|
105
|
+
"integer": FILTER_OPS,
|
|
106
|
+
"float": FILTER_OPS,
|
|
107
|
+
"string": FILTER_OPS,
|
|
108
|
+
"boolean": FILTER_OPS,
|
|
109
|
+
"date": FILTER_OPS,
|
|
110
|
+
"datetime": FILTER_OPS,
|
|
111
|
+
},
|
|
112
|
+
"aggregates": {
|
|
113
|
+
"integer": NUMBER_AGGS,
|
|
114
|
+
"float": NUMBER_AGGS,
|
|
115
|
+
"string": STRING_AGGS,
|
|
116
|
+
"boolean": STRING_AGGS,
|
|
117
|
+
"date": STRING_AGGS,
|
|
118
|
+
"datetime": STRING_AGGS,
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
def get_hosted_tables(self):
|
|
123
|
+
query = "SHOW TABLES"
|
|
124
|
+
results = run_query(self.db, query)
|
|
125
|
+
return [result[0] for result in results]
|
|
126
|
+
|
|
127
|
+
def table_schema(self, table_name, config=None):
|
|
128
|
+
query = self.sql_builder.table_schema(table_name)
|
|
129
|
+
results = run_query(self.db, query)
|
|
130
|
+
schema = {}
|
|
131
|
+
for result in results:
|
|
132
|
+
col_name = result[0]
|
|
133
|
+
if not col_name.startswith("__"):
|
|
134
|
+
schema[col_name] = clickhouse_type_to_psp(result[1])
|
|
135
|
+
|
|
136
|
+
return schema
|
|
137
|
+
|
|
138
|
+
def view_column_size(self, view_name, config):
|
|
139
|
+
query = f"SELECT COUNT() FROM system.columns WHERE table = '{view_name}'"
|
|
140
|
+
results = run_query(self.db, query)
|
|
141
|
+
gs = len(config["group_by"])
|
|
142
|
+
return results[0][0] - (
|
|
143
|
+
0 if gs == 0 else gs + (1 if len(config["split_by"]) == 0 else 0)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def table_size(self, table_name):
|
|
147
|
+
query = self.sql_builder.table_size(table_name)
|
|
148
|
+
results = run_query(self.db, query)
|
|
149
|
+
return results[0][0]
|
|
150
|
+
|
|
151
|
+
def table_make_view(self, table_name, view_name, config):
|
|
152
|
+
query = self.sql_builder.table_make_view(table_name, view_name, config)
|
|
153
|
+
run_query(self.db, query, execute=True)
|
|
154
|
+
|
|
155
|
+
def table_validate_expression(self, view_name, expression):
|
|
156
|
+
query = self.sql_builder.table_validate_expression(view_name, expression)
|
|
157
|
+
results = run_query(self.db, query)
|
|
158
|
+
return clickhouse_type_to_psp(results[0][1])
|
|
159
|
+
|
|
160
|
+
def view_delete(self, view_name):
|
|
161
|
+
query = self.sql_builder.view_delete(view_name)
|
|
162
|
+
run_query(self.db, query, execute=True)
|
|
163
|
+
|
|
164
|
+
def view_get_data(self, view_name, config, schema, viewport, data):
|
|
165
|
+
group_by = config["group_by"]
|
|
166
|
+
split_by = config["split_by"]
|
|
167
|
+
query = self.sql_builder.view_get_data(view_name, config, viewport, schema)
|
|
168
|
+
results, columns, dtypes = run_query(self.db, query, columns=True)
|
|
169
|
+
for cidx, col in enumerate(columns):
|
|
170
|
+
if cidx == 0 and len(group_by) > 0 and len(split_by) == 0:
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
if len(split_by) > 0 and not col.startswith("__ROW_PATH_"):
|
|
174
|
+
col = col.replace("_", "|")
|
|
175
|
+
|
|
176
|
+
# print(
|
|
177
|
+
# dtypes[cidx], type(dtypes[cidx]), dir(dtypes[cidx]), dtypes[cidx].name
|
|
178
|
+
# )
|
|
179
|
+
|
|
180
|
+
dtype = clickhouse_type_to_psp(str(dtypes[cidx]))
|
|
181
|
+
for ridx, row in enumerate(results):
|
|
182
|
+
grouping_id = (
|
|
183
|
+
row[0] if len(group_by) > 0 and len(split_by) == 0 else None
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
value = row[cidx]
|
|
187
|
+
if dtype == "string" and not isinstance(value, str):
|
|
188
|
+
value = str(value)
|
|
189
|
+
|
|
190
|
+
data.set_col(dtype, col, ridx, value, grouping_id)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
################################################################################
|
|
194
|
+
#
|
|
195
|
+
# ClickHouse Utils
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def clickhouse_type_to_psp(name):
|
|
199
|
+
"""Convert a ClickHouse `dtype` to a Perspective `ColumnType`."""
|
|
200
|
+
if name.startswith("Nullable(") and name.endswith(")"):
|
|
201
|
+
name = name[9:-1]
|
|
202
|
+
|
|
203
|
+
if name.startswith("Array"):
|
|
204
|
+
return "string"
|
|
205
|
+
|
|
206
|
+
if name in ("Int64", "UInt64", "Float64"):
|
|
207
|
+
return "float"
|
|
208
|
+
|
|
209
|
+
if name == "String":
|
|
210
|
+
return "string"
|
|
211
|
+
|
|
212
|
+
if name == "DateTime":
|
|
213
|
+
return "datetime"
|
|
214
|
+
|
|
215
|
+
if name == "Date":
|
|
216
|
+
return "date"
|
|
217
|
+
|
|
218
|
+
msg = f"Unknown type '{name}'"
|
|
219
|
+
raise ValueError(msg)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def run_query(db, query, execute=False, columns=False):
|
|
223
|
+
query = " ".join(query.split())
|
|
224
|
+
start = datetime.now()
|
|
225
|
+
result = None
|
|
226
|
+
try:
|
|
227
|
+
if execute:
|
|
228
|
+
db.command(query)
|
|
229
|
+
else:
|
|
230
|
+
req = db.query(query)
|
|
231
|
+
result = req.result_rows
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(e)
|
|
234
|
+
logger.error(f"{query}")
|
|
235
|
+
raise e
|
|
236
|
+
else:
|
|
237
|
+
logger.debug(f"{datetime.now() - start} {query}")
|
|
238
|
+
if columns:
|
|
239
|
+
return (
|
|
240
|
+
result,
|
|
241
|
+
req.column_names,
|
|
242
|
+
[(x.name if hasattr(x, "name") else str(x)) for x in req.column_types],
|
|
243
|
+
)
|
|
244
|
+
else:
|
|
245
|
+
return result
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
# ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
import duckdb
|
|
14
|
+
import perspective
|
|
15
|
+
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from loguru import logger
|
|
18
|
+
|
|
19
|
+
from perspective.virtual_servers import VirtualSessionModel
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
NUMBER_AGGS = [
|
|
23
|
+
"sum",
|
|
24
|
+
"count",
|
|
25
|
+
"any_value",
|
|
26
|
+
"arbitrary",
|
|
27
|
+
# "arg_max",
|
|
28
|
+
# "arg_max_null",
|
|
29
|
+
# "arg_min",
|
|
30
|
+
# "arg_min_null",
|
|
31
|
+
"array_agg",
|
|
32
|
+
"avg",
|
|
33
|
+
"bit_and",
|
|
34
|
+
"bit_or",
|
|
35
|
+
"bit_xor",
|
|
36
|
+
"bitstring_agg",
|
|
37
|
+
"bool_and",
|
|
38
|
+
"bool_or",
|
|
39
|
+
"countif",
|
|
40
|
+
"favg",
|
|
41
|
+
"fsum",
|
|
42
|
+
"geomean",
|
|
43
|
+
# "histogram",
|
|
44
|
+
# "histogram_values",
|
|
45
|
+
"kahan_sum",
|
|
46
|
+
"last",
|
|
47
|
+
# "list"
|
|
48
|
+
"max",
|
|
49
|
+
# "max_by"
|
|
50
|
+
"min",
|
|
51
|
+
# "min_by"
|
|
52
|
+
"product",
|
|
53
|
+
"string_agg",
|
|
54
|
+
"sumkahan",
|
|
55
|
+
# "weighted_avg",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
STRING_AGGS = [
|
|
59
|
+
"count",
|
|
60
|
+
"any_value",
|
|
61
|
+
"arbitrary",
|
|
62
|
+
"first",
|
|
63
|
+
"countif",
|
|
64
|
+
"last",
|
|
65
|
+
"string_agg",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
FILTER_OPS = [
|
|
69
|
+
"==",
|
|
70
|
+
"!=",
|
|
71
|
+
"LIKE",
|
|
72
|
+
"IS DISTINCT FROM",
|
|
73
|
+
"IS NOT DISTINCT FROM",
|
|
74
|
+
">=",
|
|
75
|
+
"<=",
|
|
76
|
+
">",
|
|
77
|
+
"<",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class DuckDBVirtualSession:
|
|
82
|
+
def __init__(self, callback, db):
|
|
83
|
+
self.session = perspective.VirtualServer(DuckDBVirtualSessionModel(db))
|
|
84
|
+
self.callback = callback
|
|
85
|
+
|
|
86
|
+
def handle_request(self, msg):
|
|
87
|
+
self.callback(self.session.handle_request(msg))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class DuckDBVirtualServer:
|
|
91
|
+
def __init__(self, db):
|
|
92
|
+
self.db = db
|
|
93
|
+
|
|
94
|
+
def new_session(self, callback):
|
|
95
|
+
return DuckDBVirtualSession(callback, self.db)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DuckDBVirtualSessionModel(VirtualSessionModel):
|
|
99
|
+
"""
|
|
100
|
+
An implementation of a `perspective.VirtualSessionModel` for DuckDB.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, db):
|
|
104
|
+
self.db = db
|
|
105
|
+
self.sql_builder = perspective.GenericSQLVirtualServerModel()
|
|
106
|
+
|
|
107
|
+
def get_features(self):
|
|
108
|
+
return {
|
|
109
|
+
"group_by": True,
|
|
110
|
+
"split_by": True,
|
|
111
|
+
"sort": True,
|
|
112
|
+
"expressions": True,
|
|
113
|
+
"filter_ops": {
|
|
114
|
+
"integer": FILTER_OPS,
|
|
115
|
+
"float": FILTER_OPS,
|
|
116
|
+
"string": FILTER_OPS,
|
|
117
|
+
"boolean": FILTER_OPS,
|
|
118
|
+
"date": FILTER_OPS,
|
|
119
|
+
"datetime": FILTER_OPS,
|
|
120
|
+
},
|
|
121
|
+
"aggregates": {
|
|
122
|
+
"integer": NUMBER_AGGS,
|
|
123
|
+
"float": NUMBER_AGGS,
|
|
124
|
+
"string": STRING_AGGS,
|
|
125
|
+
"boolean": STRING_AGGS,
|
|
126
|
+
"date": STRING_AGGS,
|
|
127
|
+
"datetime": STRING_AGGS,
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
def get_hosted_tables(self):
|
|
132
|
+
query = self.sql_builder.get_hosted_tables()
|
|
133
|
+
results = run_query(self.db, query)
|
|
134
|
+
return [result[2] for result in results]
|
|
135
|
+
|
|
136
|
+
def table_schema(self, table_name, config=None):
|
|
137
|
+
query = self.sql_builder.table_schema(table_name)
|
|
138
|
+
results = run_query(self.db, query)
|
|
139
|
+
schema = {}
|
|
140
|
+
for result in results:
|
|
141
|
+
col_name = result[0]
|
|
142
|
+
if not col_name.startswith("__"):
|
|
143
|
+
schema[col_name] = duckdb_type_to_psp(result[1])
|
|
144
|
+
|
|
145
|
+
return schema
|
|
146
|
+
|
|
147
|
+
def view_column_size(self, table_name, config):
|
|
148
|
+
query = self.sql_builder.view_column_size(table_name)
|
|
149
|
+
results = run_query(self.db, query)
|
|
150
|
+
gs = len(config["group_by"])
|
|
151
|
+
return results[0][0] - (
|
|
152
|
+
0 if gs == 0 else gs + (1 if len(config["split_by"]) == 0 else 0)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def table_size(self, table_name):
|
|
156
|
+
query = self.sql_builder.table_size(table_name)
|
|
157
|
+
results = run_query(self.db, query)
|
|
158
|
+
return results[0][0]
|
|
159
|
+
|
|
160
|
+
def table_make_view(self, table_name, view_name, config):
|
|
161
|
+
query = self.sql_builder.table_make_view(table_name, view_name, config)
|
|
162
|
+
run_query(self.db, query, execute=True)
|
|
163
|
+
|
|
164
|
+
def table_validate_expression(self, view_name, expression):
|
|
165
|
+
query = self.sql_builder.table_validate_expression(view_name, expression)
|
|
166
|
+
results = run_query(self.db, query)
|
|
167
|
+
return duckdb_type_to_psp(results[0][1])
|
|
168
|
+
|
|
169
|
+
def view_delete(self, view_name):
|
|
170
|
+
query = self.sql_builder.view_delete(view_name)
|
|
171
|
+
run_query(self.db, query, execute=True)
|
|
172
|
+
|
|
173
|
+
def view_get_data(self, view_name, config, schema, viewport, data):
|
|
174
|
+
group_by = config["group_by"]
|
|
175
|
+
split_by = config["split_by"]
|
|
176
|
+
query = self.sql_builder.view_get_data(view_name, config, viewport, schema)
|
|
177
|
+
results, columns, dtypes = run_query(self.db, query, columns=True)
|
|
178
|
+
for cidx, col in enumerate(columns):
|
|
179
|
+
if cidx == 0 and len(group_by) > 0 and len(split_by) == 0:
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
if len(split_by) > 0 and not col.startswith("__ROW_PATH_"):
|
|
183
|
+
col = col.replace("_", "|")
|
|
184
|
+
|
|
185
|
+
dtype = duckdb_type_to_psp(str(dtypes[cidx]))
|
|
186
|
+
for ridx, row in enumerate(results):
|
|
187
|
+
grouping_id = (
|
|
188
|
+
row[0] if len(group_by) > 0 and len(split_by) == 0 else None
|
|
189
|
+
)
|
|
190
|
+
data.set_col(dtype, col, ridx, row[cidx], grouping_id)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
################################################################################
|
|
194
|
+
#
|
|
195
|
+
# DuckDB Utils
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def duckdb_type_to_psp(name):
|
|
199
|
+
"""Convert a DuckDB `dtype` to a Perspective `ColumnType`."""
|
|
200
|
+
if name == "VARCHAR":
|
|
201
|
+
return "string"
|
|
202
|
+
if name in ("DOUBLE", "BIGINT", "HUGEINT"):
|
|
203
|
+
return "float"
|
|
204
|
+
if name == "INTEGER":
|
|
205
|
+
return "integer"
|
|
206
|
+
if name == "DATE":
|
|
207
|
+
return "date"
|
|
208
|
+
if name == "BOOLEAN":
|
|
209
|
+
return "boolean"
|
|
210
|
+
if name == "TIMESTAMP":
|
|
211
|
+
return "datetime"
|
|
212
|
+
|
|
213
|
+
msg = f"Unknown type '{name}'"
|
|
214
|
+
raise ValueError(msg)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def run_query(db, query, execute=False, columns=False):
|
|
218
|
+
query = " ".join(query.split())
|
|
219
|
+
start = datetime.now()
|
|
220
|
+
result = None
|
|
221
|
+
try:
|
|
222
|
+
if execute:
|
|
223
|
+
db.execute(query)
|
|
224
|
+
else:
|
|
225
|
+
req = db.sql(query)
|
|
226
|
+
result = req.fetchall()
|
|
227
|
+
except (duckdb.ParserException, duckdb.BinderException) as e:
|
|
228
|
+
logger.error(e)
|
|
229
|
+
logger.error(f"{query}")
|
|
230
|
+
raise e
|
|
231
|
+
else:
|
|
232
|
+
logger.debug(f"{datetime.now() - start} {query}")
|
|
233
|
+
if columns:
|
|
234
|
+
return (result, req.columns, req.dtypes)
|
|
235
|
+
else:
|
|
236
|
+
return result
|