perspective-python 5.6.0__cp311-abi3-pyemscripten_2026_0_wasm32.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 +403 -0
- perspective/extension/finos-perspective-nbextension.json +5 -0
- perspective/handlers/__init__.py +11 -0
- perspective/handlers/aiohttp.py +69 -0
- perspective/handlers/starlette.py +63 -0
- perspective/handlers/tornado.py +192 -0
- perspective/perspective.abi3.so +0 -0
- perspective/templates/exported_widget.html.template +39 -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/column_paths.py +33 -0
- perspective/tests/conftest.py +276 -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_derived_threaded.py +103 -0
- perspective/tests/multi_threaded/test_multi_threaded.py +293 -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 +88 -0
- perspective/tests/table/test_delete.py +124 -0
- perspective/tests/table/test_derived.py +145 -0
- perspective/tests/table/test_exception.py +73 -0
- perspective/tests/table/test_join.py +115 -0
- perspective/tests/table/test_leaks.py +105 -0
- perspective/tests/table/test_on_remove.py +140 -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 +500 -0
- perspective/tests/table/test_table_arrow_nested.py +535 -0
- perspective/tests/table/test_table_datetime.py +2544 -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_page_to_disk.py +304 -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 +416 -0
- perspective/tests/table/test_to_arrow_lz4.py +32 -0
- perspective/tests/table/test_to_arrow_zstd.py +41 -0
- perspective/tests/table/test_to_format.py +1030 -0
- perspective/tests/table/test_to_polars.py +26 -0
- perspective/tests/table/test_update.py +561 -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 +2503 -0
- perspective/tests/table/test_view_expression.py +2256 -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/virtual_servers/__init__.py +12 -0
- perspective/tests/virtual_servers/test_coerce_types.py +464 -0
- perspective/tests/virtual_servers/test_duckdb.py +1280 -0
- perspective/tests/virtual_servers/test_polars.py +1075 -0
- perspective/tests/virtual_servers/test_postgres.py +964 -0
- perspective/tests/widget/__init__.py +11 -0
- perspective/tests/widget/test_anywidget_smoke.py +109 -0
- perspective/tests/widget/test_widget.py +278 -0
- perspective/tests/widget/test_widget_html_export.py +113 -0
- perspective/tests/widget/test_widget_pandas.py +453 -0
- perspective/virtual_servers/__init__.py +206 -0
- perspective/virtual_servers/clickhouse.py +296 -0
- perspective/virtual_servers/duckdb.py +367 -0
- perspective/virtual_servers/polars.py +750 -0
- perspective/virtual_servers/postgres.py +448 -0
- perspective/widget/__init__.py +351 -0
- perspective/widget/static/perspective-anywidget.css +1 -0
- perspective/widget/static/perspective-anywidget.js +31 -0
- perspective/widget/viewer/__init__.py +15 -0
- perspective/widget/viewer/viewer.py +355 -0
- perspective/widget/viewer/viewer_traitlets.py +98 -0
- perspective_python-5.6.0.dist-info/METADATA +127 -0
- perspective_python-5.6.0.dist-info/RECORD +90 -0
- perspective_python-5.6.0.dist-info/WHEEL +4 -0
- perspective_python-5.6.0.dist-info/licenses/LICENSE.md +193 -0
- perspective_python-5.6.0.dist-info/licenses/LICENSE_THIRDPARTY_cargo.yml +16075 -0
- perspective_python-5.6.0.dist-info/sboms/perspective-python.cyclonedx.json +5334 -0
|
@@ -0,0 +1,1280 @@
|
|
|
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 os
|
|
14
|
+
import tempfile
|
|
15
|
+
import urllib.request
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
import duckdb
|
|
19
|
+
|
|
20
|
+
from perspective import Client
|
|
21
|
+
from perspective.virtual_servers.duckdb import DuckDBVirtualServer
|
|
22
|
+
from perspective.tests.column_paths import joined_column_paths
|
|
23
|
+
|
|
24
|
+
_SUPERSTORE_LOCAL = os.path.join(
|
|
25
|
+
os.path.dirname(__file__),
|
|
26
|
+
"..",
|
|
27
|
+
"..",
|
|
28
|
+
"..",
|
|
29
|
+
"node_modules",
|
|
30
|
+
"superstore-arrow",
|
|
31
|
+
"superstore.parquet",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_SUPERSTORE_URL = (
|
|
35
|
+
"https://cdn.jsdelivr.net/npm/superstore-arrow@3.2.0/superstore.parquet"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_superstore_parquet():
|
|
40
|
+
if os.path.exists(_SUPERSTORE_LOCAL):
|
|
41
|
+
return _SUPERSTORE_LOCAL
|
|
42
|
+
path = os.path.join(tempfile.gettempdir(), "superstore.parquet")
|
|
43
|
+
if not os.path.exists(path):
|
|
44
|
+
urllib.request.urlretrieve(_SUPERSTORE_URL, path)
|
|
45
|
+
return path
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
SUPERSTORE_PARQUET = _get_superstore_parquet()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _load_coerce_types(db):
|
|
52
|
+
"""A column of each DuckDB type whose Arrow needs coercing.
|
|
53
|
+
|
|
54
|
+
`ENUM` is the interesting one: DuckDB dictionary-encodes it and picks
|
|
55
|
+
the key width from cardinality, so it arrives as a dictionary with
|
|
56
|
+
unsigned keys rather than the `Int32` Perspective uses.
|
|
57
|
+
"""
|
|
58
|
+
db.execute("CREATE TYPE mood AS ENUM ('happy', 'sad')")
|
|
59
|
+
db.execute("""
|
|
60
|
+
CREATE TABLE coerce_types (
|
|
61
|
+
"tiny" TINYINT,
|
|
62
|
+
"small" SMALLINT,
|
|
63
|
+
"utiny" UTINYINT,
|
|
64
|
+
"usmall" USMALLINT,
|
|
65
|
+
"uint" UINTEGER,
|
|
66
|
+
"ubig" UBIGINT,
|
|
67
|
+
"big" BIGINT,
|
|
68
|
+
"float" REAL,
|
|
69
|
+
"decimal" DECIMAL(18, 3),
|
|
70
|
+
"time" TIME,
|
|
71
|
+
"timestamp" TIMESTAMP,
|
|
72
|
+
"date" DATE,
|
|
73
|
+
"enum" mood,
|
|
74
|
+
"string" VARCHAR
|
|
75
|
+
)
|
|
76
|
+
""")
|
|
77
|
+
|
|
78
|
+
db.execute("""
|
|
79
|
+
INSERT INTO coerce_types VALUES
|
|
80
|
+
(-1, -300, 255, 65535, 4294967295, 9007199254740992,
|
|
81
|
+
9007199254740992, 1.5, 1.234, TIME '01:01:01',
|
|
82
|
+
TIMESTAMP '2023-01-01 00:00:00', DATE '2023-01-01',
|
|
83
|
+
'happy', 'a'),
|
|
84
|
+
(1, 300, 0, 0, 0, 0, -9007199254740992, -1.5, -5.678,
|
|
85
|
+
TIME '00:00:01', TIMESTAMP '2023-01-02 00:00:00',
|
|
86
|
+
DATE '2023-01-02', 'sad', 'b')
|
|
87
|
+
""")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.fixture(scope="module")
|
|
91
|
+
def client():
|
|
92
|
+
db = duckdb.connect()
|
|
93
|
+
db.execute("SET default_null_order=NULLS_FIRST_ON_ASC_LAST_ON_DESC")
|
|
94
|
+
db.execute(
|
|
95
|
+
f"CREATE TABLE superstore AS SELECT * FROM read_parquet('{SUPERSTORE_PARQUET}')"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
_load_coerce_types(db)
|
|
99
|
+
server = DuckDBVirtualServer(db)
|
|
100
|
+
|
|
101
|
+
def handle_request(msg):
|
|
102
|
+
session.handle_request(msg)
|
|
103
|
+
|
|
104
|
+
def handle_response(msg):
|
|
105
|
+
c.handle_response(msg)
|
|
106
|
+
|
|
107
|
+
session = server.new_session(handle_response)
|
|
108
|
+
c = Client(handle_request)
|
|
109
|
+
return c
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TestDuckDBClient:
|
|
113
|
+
def test_get_hosted_table_names(self, client):
|
|
114
|
+
tables = client.get_hosted_table_names()
|
|
115
|
+
assert sorted(tables) == ["memory.coerce_types", "memory.superstore"]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TestDuckDBTable:
|
|
119
|
+
def test_schema(self, client):
|
|
120
|
+
table = client.open_table("memory.superstore")
|
|
121
|
+
schema = table.schema()
|
|
122
|
+
assert schema == {
|
|
123
|
+
"Product Name": "string",
|
|
124
|
+
"Ship Date": "date",
|
|
125
|
+
"City": "string",
|
|
126
|
+
"Row ID": "integer",
|
|
127
|
+
"Customer Name": "string",
|
|
128
|
+
"Quantity": "integer",
|
|
129
|
+
"Discount": "float",
|
|
130
|
+
"Sub-Category": "string",
|
|
131
|
+
"Segment": "string",
|
|
132
|
+
"Category": "string",
|
|
133
|
+
"Order Date": "date",
|
|
134
|
+
"Order ID": "string",
|
|
135
|
+
"Sales": "float",
|
|
136
|
+
"State": "string",
|
|
137
|
+
"Postal Code": "float",
|
|
138
|
+
"Country": "string",
|
|
139
|
+
"Customer ID": "string",
|
|
140
|
+
"Ship Mode": "string",
|
|
141
|
+
"Region": "string",
|
|
142
|
+
"Profit": "float",
|
|
143
|
+
"Product ID": "string",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def test_columns(self, client):
|
|
147
|
+
table = client.open_table("memory.superstore")
|
|
148
|
+
columns = table.columns()
|
|
149
|
+
assert columns == [
|
|
150
|
+
"Row ID",
|
|
151
|
+
"Order ID",
|
|
152
|
+
"Order Date",
|
|
153
|
+
"Ship Date",
|
|
154
|
+
"Ship Mode",
|
|
155
|
+
"Customer ID",
|
|
156
|
+
"Customer Name",
|
|
157
|
+
"Segment",
|
|
158
|
+
"Country",
|
|
159
|
+
"City",
|
|
160
|
+
"State",
|
|
161
|
+
"Postal Code",
|
|
162
|
+
"Region",
|
|
163
|
+
"Product ID",
|
|
164
|
+
"Category",
|
|
165
|
+
"Sub-Category",
|
|
166
|
+
"Product Name",
|
|
167
|
+
"Sales",
|
|
168
|
+
"Quantity",
|
|
169
|
+
"Discount",
|
|
170
|
+
"Profit",
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
def test_size(self, client):
|
|
174
|
+
table = client.open_table("memory.superstore")
|
|
175
|
+
size = table.size()
|
|
176
|
+
assert size == 9994
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class TestDuckDBView:
|
|
180
|
+
def test_num_rows(self, client):
|
|
181
|
+
table = client.open_table("memory.superstore")
|
|
182
|
+
view = table.view(columns=["Sales", "Profit"])
|
|
183
|
+
num_rows = view.num_rows()
|
|
184
|
+
assert num_rows == 9994
|
|
185
|
+
view.delete()
|
|
186
|
+
|
|
187
|
+
def test_num_columns(self, client):
|
|
188
|
+
table = client.open_table("memory.superstore")
|
|
189
|
+
view = table.view(columns=["Sales", "Profit", "State"])
|
|
190
|
+
num_columns = view.num_columns()
|
|
191
|
+
assert num_columns == 3
|
|
192
|
+
view.delete()
|
|
193
|
+
|
|
194
|
+
def test_schema(self, client):
|
|
195
|
+
table = client.open_table("memory.superstore")
|
|
196
|
+
view = table.view(columns=["Sales", "Profit", "State"])
|
|
197
|
+
schema = view.schema()
|
|
198
|
+
assert schema == {
|
|
199
|
+
"Sales": "float",
|
|
200
|
+
"Profit": "float",
|
|
201
|
+
"State": "string",
|
|
202
|
+
}
|
|
203
|
+
view.delete()
|
|
204
|
+
|
|
205
|
+
def test_to_json(self, client):
|
|
206
|
+
table = client.open_table("memory.superstore")
|
|
207
|
+
view = table.view(columns=["Sales", "Quantity"])
|
|
208
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
209
|
+
assert json == [
|
|
210
|
+
{"Sales": 261.96, "Quantity": 2},
|
|
211
|
+
{"Sales": 731.94, "Quantity": 3},
|
|
212
|
+
{"Sales": 14.62, "Quantity": 2},
|
|
213
|
+
{"Sales": 957.5775, "Quantity": 5},
|
|
214
|
+
{"Sales": 22.368, "Quantity": 2},
|
|
215
|
+
]
|
|
216
|
+
view.delete()
|
|
217
|
+
|
|
218
|
+
def test_to_columns(self, client):
|
|
219
|
+
table = client.open_table("memory.superstore")
|
|
220
|
+
view = table.view(columns=["Sales", "Quantity"])
|
|
221
|
+
columns = view.to_columns(start_row=0, end_row=5)
|
|
222
|
+
assert columns == {
|
|
223
|
+
"Sales": [261.96, 731.94, 14.62, 957.5775, 22.368],
|
|
224
|
+
"Quantity": [2, 3, 2, 5, 2],
|
|
225
|
+
}
|
|
226
|
+
view.delete()
|
|
227
|
+
|
|
228
|
+
def test_column_paths(self, client):
|
|
229
|
+
table = client.open_table("memory.superstore")
|
|
230
|
+
view = table.view(columns=["Sales", "Profit", "State"])
|
|
231
|
+
paths = joined_column_paths(view)
|
|
232
|
+
assert paths == ["Sales", "Profit", "State"]
|
|
233
|
+
view.delete()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class TestDuckDBGroupBy:
|
|
237
|
+
def test_single_group_by(self, client):
|
|
238
|
+
table = client.open_table("memory.superstore")
|
|
239
|
+
view = table.view(
|
|
240
|
+
columns=["Sales"],
|
|
241
|
+
group_by=["Region"],
|
|
242
|
+
aggregates={"Sales": "sum"},
|
|
243
|
+
)
|
|
244
|
+
num_rows = view.num_rows()
|
|
245
|
+
assert num_rows == 5
|
|
246
|
+
json = view.to_json()
|
|
247
|
+
assert json == [
|
|
248
|
+
{"__ROW_PATH__": [], "Sales": 2297200.860299955},
|
|
249
|
+
{"__ROW_PATH__": ["Central"], "Sales": 501239.8908000005},
|
|
250
|
+
{"__ROW_PATH__": ["East"], "Sales": 678781.2399999979},
|
|
251
|
+
{"__ROW_PATH__": ["South"], "Sales": 391721.9050000003},
|
|
252
|
+
{"__ROW_PATH__": ["West"], "Sales": 725457.8245000006},
|
|
253
|
+
]
|
|
254
|
+
view.delete()
|
|
255
|
+
|
|
256
|
+
def test_multi_level_group_by(self, client):
|
|
257
|
+
table = client.open_table("memory.superstore")
|
|
258
|
+
view = table.view(
|
|
259
|
+
columns=["Sales"],
|
|
260
|
+
group_by=["Region", "Category"],
|
|
261
|
+
aggregates={"Sales": "sum"},
|
|
262
|
+
)
|
|
263
|
+
json = view.to_json()
|
|
264
|
+
assert json == [
|
|
265
|
+
{"__ROW_PATH__": [], "Sales": 2297200.860299955},
|
|
266
|
+
{"__ROW_PATH__": ["Central"], "Sales": 501239.8908000005},
|
|
267
|
+
{"__ROW_PATH__": ["Central", "Furniture"], "Sales": 163797.16380000004},
|
|
268
|
+
{
|
|
269
|
+
"__ROW_PATH__": ["Central", "Office Supplies"],
|
|
270
|
+
"Sales": 167026.41500000027,
|
|
271
|
+
},
|
|
272
|
+
{"__ROW_PATH__": ["Central", "Technology"], "Sales": 170416.3119999999},
|
|
273
|
+
{"__ROW_PATH__": ["East"], "Sales": 678781.2399999979},
|
|
274
|
+
{"__ROW_PATH__": ["East", "Furniture"], "Sales": 208291.20400000009},
|
|
275
|
+
{"__ROW_PATH__": ["East", "Office Supplies"], "Sales": 205516.0549999999},
|
|
276
|
+
{"__ROW_PATH__": ["East", "Technology"], "Sales": 264973.9810000003},
|
|
277
|
+
{"__ROW_PATH__": ["South"], "Sales": 391721.9050000003},
|
|
278
|
+
{"__ROW_PATH__": ["South", "Furniture"], "Sales": 117298.6840000001},
|
|
279
|
+
{"__ROW_PATH__": ["South", "Office Supplies"], "Sales": 125651.31299999992},
|
|
280
|
+
{"__ROW_PATH__": ["South", "Technology"], "Sales": 148771.9079999999},
|
|
281
|
+
{"__ROW_PATH__": ["West"], "Sales": 725457.8245000006},
|
|
282
|
+
{"__ROW_PATH__": ["West", "Furniture"], "Sales": 252612.7435000003},
|
|
283
|
+
{"__ROW_PATH__": ["West", "Office Supplies"], "Sales": 220853.24900000007},
|
|
284
|
+
{"__ROW_PATH__": ["West", "Technology"], "Sales": 251991.83199999997},
|
|
285
|
+
]
|
|
286
|
+
view.delete()
|
|
287
|
+
|
|
288
|
+
def test_group_by_with_count_aggregate(self, client):
|
|
289
|
+
table = client.open_table("memory.superstore")
|
|
290
|
+
view = table.view(
|
|
291
|
+
columns=["Sales"],
|
|
292
|
+
group_by=["Region"],
|
|
293
|
+
aggregates={"Sales": "count"},
|
|
294
|
+
)
|
|
295
|
+
json = view.to_json()
|
|
296
|
+
assert json == [
|
|
297
|
+
{"__ROW_PATH__": [], "Sales": 9994},
|
|
298
|
+
{"__ROW_PATH__": ["Central"], "Sales": 2323},
|
|
299
|
+
{"__ROW_PATH__": ["East"], "Sales": 2848},
|
|
300
|
+
{"__ROW_PATH__": ["South"], "Sales": 1620},
|
|
301
|
+
{"__ROW_PATH__": ["West"], "Sales": 3203},
|
|
302
|
+
]
|
|
303
|
+
view.delete()
|
|
304
|
+
|
|
305
|
+
def test_group_by_with_avg_aggregate(self, client):
|
|
306
|
+
table = client.open_table("memory.superstore")
|
|
307
|
+
view = table.view(
|
|
308
|
+
columns=["Sales"],
|
|
309
|
+
group_by=["Category"],
|
|
310
|
+
aggregates={"Sales": "avg"},
|
|
311
|
+
)
|
|
312
|
+
json = view.to_json()
|
|
313
|
+
assert json == [
|
|
314
|
+
{"__ROW_PATH__": [], "Sales": 229.8580008304938},
|
|
315
|
+
{"__ROW_PATH__": ["Furniture"], "Sales": 349.83488698727007},
|
|
316
|
+
{"__ROW_PATH__": ["Office Supplies"], "Sales": 119.32410089611732},
|
|
317
|
+
{"__ROW_PATH__": ["Technology"], "Sales": 452.70927612344155},
|
|
318
|
+
]
|
|
319
|
+
view.delete()
|
|
320
|
+
|
|
321
|
+
def test_group_by_with_min_aggregate(self, client):
|
|
322
|
+
table = client.open_table("memory.superstore")
|
|
323
|
+
view = table.view(
|
|
324
|
+
columns=["Quantity"],
|
|
325
|
+
group_by=["Region"],
|
|
326
|
+
aggregates={"Quantity": "min"},
|
|
327
|
+
)
|
|
328
|
+
json = view.to_json()
|
|
329
|
+
assert json == [
|
|
330
|
+
{"__ROW_PATH__": [], "Quantity": 1},
|
|
331
|
+
{"__ROW_PATH__": ["Central"], "Quantity": 1},
|
|
332
|
+
{"__ROW_PATH__": ["East"], "Quantity": 1},
|
|
333
|
+
{"__ROW_PATH__": ["South"], "Quantity": 1},
|
|
334
|
+
{"__ROW_PATH__": ["West"], "Quantity": 1},
|
|
335
|
+
]
|
|
336
|
+
view.delete()
|
|
337
|
+
|
|
338
|
+
def test_group_by_with_max_aggregate(self, client):
|
|
339
|
+
table = client.open_table("memory.superstore")
|
|
340
|
+
view = table.view(
|
|
341
|
+
columns=["Quantity"],
|
|
342
|
+
group_by=["Region"],
|
|
343
|
+
aggregates={"Quantity": "max"},
|
|
344
|
+
)
|
|
345
|
+
json = view.to_json()
|
|
346
|
+
assert json == [
|
|
347
|
+
{"__ROW_PATH__": [], "Quantity": 14},
|
|
348
|
+
{"__ROW_PATH__": ["Central"], "Quantity": 14},
|
|
349
|
+
{"__ROW_PATH__": ["East"], "Quantity": 14},
|
|
350
|
+
{"__ROW_PATH__": ["South"], "Quantity": 14},
|
|
351
|
+
{"__ROW_PATH__": ["West"], "Quantity": 14},
|
|
352
|
+
]
|
|
353
|
+
view.delete()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class TestDuckDBSplitBy:
|
|
357
|
+
def test_single_split_by(self, client):
|
|
358
|
+
table = client.open_table("memory.superstore")
|
|
359
|
+
view = table.view(
|
|
360
|
+
columns=["Sales"],
|
|
361
|
+
split_by=["Region"],
|
|
362
|
+
group_by=["Category"],
|
|
363
|
+
aggregates={"Sales": "sum"},
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
column_paths = joined_column_paths(view)
|
|
367
|
+
assert column_paths == [
|
|
368
|
+
"Central|Sales",
|
|
369
|
+
"East|Sales",
|
|
370
|
+
"South|Sales",
|
|
371
|
+
"West|Sales",
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
json = view.to_json()
|
|
375
|
+
assert json == [
|
|
376
|
+
{
|
|
377
|
+
"__ROW_PATH__": [],
|
|
378
|
+
"Central|Sales": 501239.8908000005,
|
|
379
|
+
"East|Sales": 678781.2399999979,
|
|
380
|
+
"South|Sales": 391721.9050000003,
|
|
381
|
+
"West|Sales": 725457.8245000006,
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
"__ROW_PATH__": ["Furniture"],
|
|
385
|
+
"Central|Sales": 163797.16380000004,
|
|
386
|
+
"East|Sales": 208291.20400000009,
|
|
387
|
+
"South|Sales": 117298.6840000001,
|
|
388
|
+
"West|Sales": 252612.7435000003,
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
"__ROW_PATH__": ["Office Supplies"],
|
|
392
|
+
"Central|Sales": 167026.41500000027,
|
|
393
|
+
"East|Sales": 205516.0549999999,
|
|
394
|
+
"South|Sales": 125651.31299999992,
|
|
395
|
+
"West|Sales": 220853.24900000007,
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
"__ROW_PATH__": ["Technology"],
|
|
399
|
+
"Central|Sales": 170416.3119999999,
|
|
400
|
+
"East|Sales": 264973.9810000003,
|
|
401
|
+
"South|Sales": 148771.9079999999,
|
|
402
|
+
"West|Sales": 251991.83199999997,
|
|
403
|
+
},
|
|
404
|
+
]
|
|
405
|
+
view.delete()
|
|
406
|
+
|
|
407
|
+
def test_split_by_without_group_by(self, client):
|
|
408
|
+
table = client.open_table("memory.superstore")
|
|
409
|
+
view = table.view(
|
|
410
|
+
columns=["Sales"],
|
|
411
|
+
split_by=["Category"],
|
|
412
|
+
)
|
|
413
|
+
paths = joined_column_paths(view)
|
|
414
|
+
assert any("Furniture" in c for c in paths)
|
|
415
|
+
assert any("Office Supplies" in c for c in paths)
|
|
416
|
+
assert any("Technology" in c for c in paths)
|
|
417
|
+
view.delete()
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class TestDuckDBFilter:
|
|
421
|
+
def test_filter_with_equals(self, client):
|
|
422
|
+
table = client.open_table("memory.superstore")
|
|
423
|
+
view = table.view(
|
|
424
|
+
columns=["Sales", "Region"],
|
|
425
|
+
filter=[["Region", "==", "West"]],
|
|
426
|
+
)
|
|
427
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
428
|
+
assert json == [
|
|
429
|
+
{"Sales": 14.62, "Region": "West"},
|
|
430
|
+
{"Sales": 48.86, "Region": "West"},
|
|
431
|
+
{"Sales": 7.28, "Region": "West"},
|
|
432
|
+
{"Sales": 907.152, "Region": "West"},
|
|
433
|
+
{"Sales": 18.504, "Region": "West"},
|
|
434
|
+
]
|
|
435
|
+
view.delete()
|
|
436
|
+
|
|
437
|
+
def test_filter_with_not_equals(self, client):
|
|
438
|
+
table = client.open_table("memory.superstore")
|
|
439
|
+
view = table.view(
|
|
440
|
+
columns=["Sales", "Region"],
|
|
441
|
+
filter=[["Region", "!=", "West"]],
|
|
442
|
+
)
|
|
443
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
444
|
+
assert json == [
|
|
445
|
+
{"Sales": 261.96, "Region": "South"},
|
|
446
|
+
{"Sales": 731.94, "Region": "South"},
|
|
447
|
+
{"Sales": 957.5775, "Region": "South"},
|
|
448
|
+
{"Sales": 22.368, "Region": "South"},
|
|
449
|
+
{"Sales": 15.552, "Region": "South"},
|
|
450
|
+
]
|
|
451
|
+
view.delete()
|
|
452
|
+
|
|
453
|
+
def test_filter_with_greater_than(self, client):
|
|
454
|
+
table = client.open_table("memory.superstore")
|
|
455
|
+
view = table.view(
|
|
456
|
+
columns=["Sales", "Quantity"],
|
|
457
|
+
filter=[["Quantity", ">", 5]],
|
|
458
|
+
)
|
|
459
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
460
|
+
assert json == [
|
|
461
|
+
{"Sales": 48.86, "Quantity": 7},
|
|
462
|
+
{"Sales": 907.152, "Quantity": 6},
|
|
463
|
+
{"Sales": 1706.184, "Quantity": 9},
|
|
464
|
+
{"Sales": 665.88, "Quantity": 6},
|
|
465
|
+
{"Sales": 19.46, "Quantity": 7},
|
|
466
|
+
]
|
|
467
|
+
view.delete()
|
|
468
|
+
|
|
469
|
+
def test_filter_with_less_than(self, client):
|
|
470
|
+
table = client.open_table("memory.superstore")
|
|
471
|
+
view = table.view(
|
|
472
|
+
columns=["Sales", "Quantity"],
|
|
473
|
+
filter=[["Quantity", "<", 3]],
|
|
474
|
+
)
|
|
475
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
476
|
+
assert json == [
|
|
477
|
+
{"Sales": 261.96, "Quantity": 2},
|
|
478
|
+
{"Sales": 14.62, "Quantity": 2},
|
|
479
|
+
{"Sales": 22.368, "Quantity": 2},
|
|
480
|
+
{"Sales": 55.5, "Quantity": 2},
|
|
481
|
+
{"Sales": 8.56, "Quantity": 2},
|
|
482
|
+
]
|
|
483
|
+
view.delete()
|
|
484
|
+
|
|
485
|
+
def test_filter_with_greater_than_or_equal(self, client):
|
|
486
|
+
table = client.open_table("memory.superstore")
|
|
487
|
+
view = table.view(
|
|
488
|
+
columns=["Sales", "Quantity"],
|
|
489
|
+
filter=[["Quantity", ">=", 10]],
|
|
490
|
+
)
|
|
491
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
492
|
+
assert json == [
|
|
493
|
+
{"Sales": 40.096, "Quantity": 14},
|
|
494
|
+
{"Sales": 43.12, "Quantity": 14},
|
|
495
|
+
{"Sales": 384.45, "Quantity": 11},
|
|
496
|
+
{"Sales": 3347.37, "Quantity": 13},
|
|
497
|
+
{"Sales": 100.24, "Quantity": 10},
|
|
498
|
+
]
|
|
499
|
+
view.delete()
|
|
500
|
+
|
|
501
|
+
def test_filter_with_less_than_or_equal(self, client):
|
|
502
|
+
table = client.open_table("memory.superstore")
|
|
503
|
+
view = table.view(
|
|
504
|
+
columns=["Sales", "Quantity"],
|
|
505
|
+
filter=[["Quantity", "<=", 2]],
|
|
506
|
+
)
|
|
507
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
508
|
+
assert json == [
|
|
509
|
+
{"Sales": 261.96, "Quantity": 2},
|
|
510
|
+
{"Sales": 14.62, "Quantity": 2},
|
|
511
|
+
{"Sales": 22.368, "Quantity": 2},
|
|
512
|
+
{"Sales": 55.5, "Quantity": 2},
|
|
513
|
+
{"Sales": 8.56, "Quantity": 2},
|
|
514
|
+
]
|
|
515
|
+
view.delete()
|
|
516
|
+
|
|
517
|
+
def test_filter_with_like(self, client):
|
|
518
|
+
table = client.open_table("memory.superstore")
|
|
519
|
+
view = table.view(
|
|
520
|
+
columns=["Sales", "State"],
|
|
521
|
+
filter=[["State", "LIKE", "Cal%"]],
|
|
522
|
+
)
|
|
523
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
524
|
+
assert json == [
|
|
525
|
+
{"Sales": 14.62, "State": "California"},
|
|
526
|
+
{"Sales": 48.86, "State": "California"},
|
|
527
|
+
{"Sales": 7.28, "State": "California"},
|
|
528
|
+
{"Sales": 907.152, "State": "California"},
|
|
529
|
+
{"Sales": 18.504, "State": "California"},
|
|
530
|
+
]
|
|
531
|
+
view.delete()
|
|
532
|
+
|
|
533
|
+
def test_multiple_filters(self, client):
|
|
534
|
+
table = client.open_table("memory.superstore")
|
|
535
|
+
view = table.view(
|
|
536
|
+
columns=["Sales", "Region", "Quantity"],
|
|
537
|
+
filter=[
|
|
538
|
+
["Region", "==", "West"],
|
|
539
|
+
["Quantity", ">", 3],
|
|
540
|
+
],
|
|
541
|
+
)
|
|
542
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
543
|
+
assert json == [
|
|
544
|
+
{"Sales": 48.86, "Region": "West", "Quantity": 7},
|
|
545
|
+
{"Sales": 7.28, "Region": "West", "Quantity": 4},
|
|
546
|
+
{"Sales": 907.152, "Region": "West", "Quantity": 6},
|
|
547
|
+
{"Sales": 114.9, "Region": "West", "Quantity": 5},
|
|
548
|
+
{"Sales": 1706.184, "Region": "West", "Quantity": 9},
|
|
549
|
+
]
|
|
550
|
+
view.delete()
|
|
551
|
+
|
|
552
|
+
def test_filter_with_group_by(self, client):
|
|
553
|
+
table = client.open_table("memory.superstore")
|
|
554
|
+
view = table.view(
|
|
555
|
+
columns=["Sales"],
|
|
556
|
+
group_by=["Category"],
|
|
557
|
+
filter=[["Region", "==", "West"]],
|
|
558
|
+
aggregates={"Sales": "sum"},
|
|
559
|
+
)
|
|
560
|
+
num_rows = view.num_rows()
|
|
561
|
+
assert num_rows == 4
|
|
562
|
+
json = view.to_json()
|
|
563
|
+
assert json == [
|
|
564
|
+
{"__ROW_PATH__": [], "Sales": 725457.8245000006},
|
|
565
|
+
{"__ROW_PATH__": ["Furniture"], "Sales": 252612.7435000003},
|
|
566
|
+
{"__ROW_PATH__": ["Office Supplies"], "Sales": 220853.24900000007},
|
|
567
|
+
{"__ROW_PATH__": ["Technology"], "Sales": 251991.83199999997},
|
|
568
|
+
]
|
|
569
|
+
view.delete()
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
class TestDuckDBSort:
|
|
573
|
+
def test_sort_ascending(self, client):
|
|
574
|
+
table = client.open_table("memory.superstore")
|
|
575
|
+
view = table.view(
|
|
576
|
+
columns=["Sales", "Quantity"],
|
|
577
|
+
sort=[["Sales", "asc"]],
|
|
578
|
+
)
|
|
579
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
580
|
+
assert json == [
|
|
581
|
+
{"Sales": 0.444, "Quantity": 1},
|
|
582
|
+
{"Sales": 0.556, "Quantity": 1},
|
|
583
|
+
{"Sales": 0.836, "Quantity": 1},
|
|
584
|
+
{"Sales": 0.852, "Quantity": 1},
|
|
585
|
+
{"Sales": 0.876, "Quantity": 1},
|
|
586
|
+
]
|
|
587
|
+
view.delete()
|
|
588
|
+
|
|
589
|
+
def test_sort_descending(self, client):
|
|
590
|
+
table = client.open_table("memory.superstore")
|
|
591
|
+
view = table.view(
|
|
592
|
+
columns=["Sales", "Quantity"],
|
|
593
|
+
sort=[["Sales", "desc"]],
|
|
594
|
+
)
|
|
595
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
596
|
+
assert json == [
|
|
597
|
+
{"Sales": 22638.48, "Quantity": 6},
|
|
598
|
+
{"Sales": 17499.95, "Quantity": 5},
|
|
599
|
+
{"Sales": 13999.96, "Quantity": 4},
|
|
600
|
+
{"Sales": 11199.968, "Quantity": 4},
|
|
601
|
+
{"Sales": 10499.97, "Quantity": 3},
|
|
602
|
+
]
|
|
603
|
+
view.delete()
|
|
604
|
+
|
|
605
|
+
def test_sort_with_group_by(self, client):
|
|
606
|
+
table = client.open_table("memory.superstore")
|
|
607
|
+
view = table.view(
|
|
608
|
+
columns=["Sales"],
|
|
609
|
+
group_by=["Region"],
|
|
610
|
+
sort=[["Sales", "desc"]],
|
|
611
|
+
aggregates={"Sales": "sum"},
|
|
612
|
+
)
|
|
613
|
+
json = view.to_json()
|
|
614
|
+
assert json == [
|
|
615
|
+
{"__ROW_PATH__": [], "Sales": 2297200.860299955},
|
|
616
|
+
{"__ROW_PATH__": ["West"], "Sales": 725457.8245000006},
|
|
617
|
+
{"__ROW_PATH__": ["East"], "Sales": 678781.2399999979},
|
|
618
|
+
{"__ROW_PATH__": ["Central"], "Sales": 501239.8908000005},
|
|
619
|
+
{"__ROW_PATH__": ["South"], "Sales": 391721.9050000003},
|
|
620
|
+
]
|
|
621
|
+
view.delete()
|
|
622
|
+
|
|
623
|
+
def test_multi_column_sort(self, client):
|
|
624
|
+
table = client.open_table("memory.superstore")
|
|
625
|
+
view = table.view(
|
|
626
|
+
columns=["Region", "Sales", "Quantity"],
|
|
627
|
+
sort=[
|
|
628
|
+
["Region", "asc"],
|
|
629
|
+
["Sales", "desc"],
|
|
630
|
+
],
|
|
631
|
+
)
|
|
632
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
633
|
+
assert json == [
|
|
634
|
+
{"Region": "Central", "Sales": 17499.95, "Quantity": 5},
|
|
635
|
+
{"Region": "Central", "Sales": 9892.74, "Quantity": 13},
|
|
636
|
+
{"Region": "Central", "Sales": 9449.95, "Quantity": 5},
|
|
637
|
+
{"Region": "Central", "Sales": 8159.952, "Quantity": 8},
|
|
638
|
+
{"Region": "Central", "Sales": 5443.96, "Quantity": 4},
|
|
639
|
+
]
|
|
640
|
+
view.delete()
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
DESCRIBE_CONFIGS = [
|
|
644
|
+
dict(columns=["Sales", "Quantity", "Region"]),
|
|
645
|
+
dict(
|
|
646
|
+
columns=["Sales", "Quantity"],
|
|
647
|
+
group_by=["Region"],
|
|
648
|
+
aggregates={"Sales": "sum", "Quantity": "avg"},
|
|
649
|
+
),
|
|
650
|
+
dict(columns=["Sales", "Quantity"], group_by=["Region"], group_rollup_mode="flat"),
|
|
651
|
+
dict(columns=["Sales"], group_by=["Region"], sort=[["Quantity", "desc"]]),
|
|
652
|
+
dict(columns=["Sales", "Quantity"], split_by=["Region"]),
|
|
653
|
+
dict(columns=["Sales", "Quantity"], split_by=["Region"], aggregates={"Quantity": "avg"}),
|
|
654
|
+
dict(
|
|
655
|
+
columns=["Sales", "Quantity"],
|
|
656
|
+
group_by=["Category"],
|
|
657
|
+
split_by=["Region"],
|
|
658
|
+
aggregates={"Sales": "sum", "Quantity": "avg"},
|
|
659
|
+
),
|
|
660
|
+
dict(
|
|
661
|
+
columns=["Sales"],
|
|
662
|
+
group_by=["Category"],
|
|
663
|
+
split_by=["Region"],
|
|
664
|
+
group_rollup_mode="flat",
|
|
665
|
+
),
|
|
666
|
+
dict(
|
|
667
|
+
columns=["Sales"],
|
|
668
|
+
group_by=["Category"],
|
|
669
|
+
split_by=["Region"],
|
|
670
|
+
split_rollup_mode="rollup",
|
|
671
|
+
),
|
|
672
|
+
dict(columns=["Sales", "Quantity"], group_rollup_mode="total", aggregates={"Quantity": "avg"}),
|
|
673
|
+
dict(
|
|
674
|
+
columns=["Sales", "Quantity"],
|
|
675
|
+
split_by=["Region"],
|
|
676
|
+
group_rollup_mode="total",
|
|
677
|
+
aggregates={"Quantity": "avg"},
|
|
678
|
+
),
|
|
679
|
+
dict(columns=["Sales", "double"], expressions={"double": '"Sales" * 2'}),
|
|
680
|
+
dict(
|
|
681
|
+
columns=["total"],
|
|
682
|
+
group_by=["Region"],
|
|
683
|
+
expressions={"total": '"Sales" + "Profit"'},
|
|
684
|
+
aggregates={"total": "sum"},
|
|
685
|
+
),
|
|
686
|
+
]
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
class TestDuckDBDescribe:
|
|
690
|
+
@pytest.mark.parametrize("config", DESCRIBE_CONFIGS)
|
|
691
|
+
def test_parity_with_view(self, client, config):
|
|
692
|
+
table = client.open_table("memory.superstore")
|
|
693
|
+
verdict = table.describe(**config)
|
|
694
|
+
view = table.view(**config)
|
|
695
|
+
assert sorted(verdict.keys()) == ["expression_schema", "view_schema"]
|
|
696
|
+
assert verdict["view_schema"] == view.schema()
|
|
697
|
+
assert sorted(verdict["expression_schema"].keys()) == sorted(
|
|
698
|
+
config.get("expressions", {}).keys()
|
|
699
|
+
)
|
|
700
|
+
view.delete()
|
|
701
|
+
|
|
702
|
+
def test_attributes_a_bad_expression(self, client):
|
|
703
|
+
table = client.open_table("memory.superstore")
|
|
704
|
+
verdict = table.describe(
|
|
705
|
+
columns=["Sales", "good", "bad"],
|
|
706
|
+
expressions={"good": '"Sales" * 2', "bad": '"nope" * 2'},
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
assert sorted(verdict.keys()) == ["expression_errors", "expression_schema"]
|
|
710
|
+
assert verdict["expression_schema"] == {"good": "float"}
|
|
711
|
+
assert list(verdict["expression_errors"].keys()) == ["bad"]
|
|
712
|
+
assert "nope" in verdict["expression_errors"]["bad"]["error_message"]
|
|
713
|
+
with pytest.raises(Exception):
|
|
714
|
+
table.view(
|
|
715
|
+
columns=["Sales", "good", "bad"],
|
|
716
|
+
expressions={"good": '"Sales" * 2', "bad": '"nope" * 2'},
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
def test_unused_invalid_expression_is_still_rejected(self, client):
|
|
720
|
+
table = client.open_table("memory.superstore")
|
|
721
|
+
verdict = table.describe(columns=["Sales"], expressions={"bad": '"nope" * 2'})
|
|
722
|
+
assert list(verdict["expression_errors"].keys()) == ["bad"]
|
|
723
|
+
|
|
724
|
+
def test_bad_column_is_a_config_error(self, client):
|
|
725
|
+
table = client.open_table("memory.superstore")
|
|
726
|
+
verdict = table.describe(columns=["Sales", "nope"])
|
|
727
|
+
assert list(verdict.keys()) == ["config_error"]
|
|
728
|
+
|
|
729
|
+
def test_validate_expressions(self, client):
|
|
730
|
+
table = client.open_table("memory.superstore")
|
|
731
|
+
expressions = {"good": '"Sales" * 2', "bad": '"nope" * 2'}
|
|
732
|
+
validated = table.validate_expressions(expressions)
|
|
733
|
+
assert validated["expression_schema"] == {"good": "float"}
|
|
734
|
+
assert list(validated["errors"].keys()) == ["bad"]
|
|
735
|
+
assert validated["expression_alias"] == expressions
|
|
736
|
+
|
|
737
|
+
def test_view_expression_schema(self, client):
|
|
738
|
+
table = client.open_table("memory.superstore")
|
|
739
|
+
view = table.view(columns=["double"], expressions={"double": '"Sales" * 2'})
|
|
740
|
+
assert view.expression_schema() == {"double": "float"}
|
|
741
|
+
view.delete()
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
class TestDuckDBExpressions:
|
|
745
|
+
def test_simple_expression(self, client):
|
|
746
|
+
table = client.open_table("memory.superstore")
|
|
747
|
+
view = table.view(
|
|
748
|
+
columns=["Sales", "doublesales"],
|
|
749
|
+
expressions={"doublesales": '"Sales" * 2'},
|
|
750
|
+
)
|
|
751
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
752
|
+
assert json == [
|
|
753
|
+
{"Sales": 261.96, "doublesales": 523.92},
|
|
754
|
+
{"Sales": 731.94, "doublesales": 1463.88},
|
|
755
|
+
{"Sales": 14.62, "doublesales": 29.24},
|
|
756
|
+
{"Sales": 957.5775, "doublesales": 1915.155},
|
|
757
|
+
{"Sales": 22.368, "doublesales": 44.736},
|
|
758
|
+
]
|
|
759
|
+
view.delete()
|
|
760
|
+
|
|
761
|
+
def test_expression_with_multiple_columns(self, client):
|
|
762
|
+
table = client.open_table("memory.superstore")
|
|
763
|
+
view = table.view(
|
|
764
|
+
columns=["Sales", "Profit", "margin"],
|
|
765
|
+
expressions={"margin": '"Profit" / "Sales"'},
|
|
766
|
+
)
|
|
767
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
768
|
+
assert json == [
|
|
769
|
+
{"Sales": 261.96, "Profit": 41.9136, "margin": 0.16000000000000003},
|
|
770
|
+
{"Sales": 731.94, "Profit": 219.582, "margin": 0.3},
|
|
771
|
+
{"Sales": 14.62, "Profit": 6.8714, "margin": 0.47000000000000003},
|
|
772
|
+
{"Sales": 957.5775, "Profit": -383.031, "margin": -0.4},
|
|
773
|
+
{"Sales": 22.368, "Profit": 2.5164, "margin": 0.1125},
|
|
774
|
+
]
|
|
775
|
+
view.delete()
|
|
776
|
+
|
|
777
|
+
def test_expression_with_group_by(self, client):
|
|
778
|
+
table = client.open_table("memory.superstore")
|
|
779
|
+
view = table.view(
|
|
780
|
+
columns=["total"],
|
|
781
|
+
group_by=["Region"],
|
|
782
|
+
expressions={"total": '"Sales" + "Profit"'},
|
|
783
|
+
aggregates={"total": "sum"},
|
|
784
|
+
)
|
|
785
|
+
json = view.to_json()
|
|
786
|
+
assert json == [
|
|
787
|
+
{"__ROW_PATH__": [], "total": 2583597.882000014},
|
|
788
|
+
{"__ROW_PATH__": ["Central"], "total": 540946.2532999996},
|
|
789
|
+
{"__ROW_PATH__": ["East"], "total": 770304.0199999991},
|
|
790
|
+
{"__ROW_PATH__": ["South"], "total": 438471.33530000027},
|
|
791
|
+
{"__ROW_PATH__": ["West"], "total": 833876.2733999988},
|
|
792
|
+
]
|
|
793
|
+
view.delete()
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
class TestDuckDBViewport:
|
|
797
|
+
def test_start_row_and_end_row(self, client):
|
|
798
|
+
table = client.open_table("memory.superstore")
|
|
799
|
+
view = table.view(columns=["Sales", "Profit"])
|
|
800
|
+
json = view.to_json(start_row=10, end_row=15)
|
|
801
|
+
assert json == [
|
|
802
|
+
{"Sales": 1706.184, "Profit": 85.3092},
|
|
803
|
+
{"Sales": 911.424, "Profit": 68.3568},
|
|
804
|
+
{"Sales": 15.552, "Profit": 5.4432},
|
|
805
|
+
{"Sales": 407.976, "Profit": 132.5922},
|
|
806
|
+
{"Sales": 68.81, "Profit": -123.858},
|
|
807
|
+
]
|
|
808
|
+
view.delete()
|
|
809
|
+
|
|
810
|
+
def test_start_col_and_end_col(self, client):
|
|
811
|
+
table = client.open_table("memory.superstore")
|
|
812
|
+
view = table.view(
|
|
813
|
+
columns=["Sales", "Profit", "Quantity", "Discount"],
|
|
814
|
+
)
|
|
815
|
+
json = view.to_json(start_row=0, end_row=5, start_col=1, end_col=3)
|
|
816
|
+
assert json == [
|
|
817
|
+
{"Profit": 41.9136, "Quantity": 2},
|
|
818
|
+
{"Profit": 219.582, "Quantity": 3},
|
|
819
|
+
{"Profit": 6.8714, "Quantity": 2},
|
|
820
|
+
{"Profit": -383.031, "Quantity": 5},
|
|
821
|
+
{"Profit": 2.5164, "Quantity": 2},
|
|
822
|
+
]
|
|
823
|
+
view.delete()
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
class TestDuckDBDataTypes:
|
|
827
|
+
def test_integer_columns(self, client):
|
|
828
|
+
table = client.open_table("memory.superstore")
|
|
829
|
+
view = table.view(columns=["Quantity"])
|
|
830
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
831
|
+
assert json == [
|
|
832
|
+
{"Quantity": 2},
|
|
833
|
+
{"Quantity": 3},
|
|
834
|
+
{"Quantity": 2},
|
|
835
|
+
{"Quantity": 5},
|
|
836
|
+
{"Quantity": 2},
|
|
837
|
+
]
|
|
838
|
+
view.delete()
|
|
839
|
+
|
|
840
|
+
def test_float_columns(self, client):
|
|
841
|
+
table = client.open_table("memory.superstore")
|
|
842
|
+
view = table.view(columns=["Sales", "Profit"])
|
|
843
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
844
|
+
assert json == [
|
|
845
|
+
{"Sales": 261.96, "Profit": 41.9136},
|
|
846
|
+
{"Sales": 731.94, "Profit": 219.582},
|
|
847
|
+
{"Sales": 14.62, "Profit": 6.8714},
|
|
848
|
+
{"Sales": 957.5775, "Profit": -383.031},
|
|
849
|
+
{"Sales": 22.368, "Profit": 2.5164},
|
|
850
|
+
]
|
|
851
|
+
view.delete()
|
|
852
|
+
|
|
853
|
+
def test_string_columns(self, client):
|
|
854
|
+
table = client.open_table("memory.superstore")
|
|
855
|
+
view = table.view(columns=["Region", "State", "City"])
|
|
856
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
857
|
+
assert json == [
|
|
858
|
+
{"Region": "South", "State": "Kentucky", "City": "Henderson"},
|
|
859
|
+
{"Region": "South", "State": "Kentucky", "City": "Henderson"},
|
|
860
|
+
{"Region": "West", "State": "California", "City": "Los Angeles"},
|
|
861
|
+
{"Region": "South", "State": "Florida", "City": "Fort Lauderdale"},
|
|
862
|
+
{"Region": "South", "State": "Florida", "City": "Fort Lauderdale"},
|
|
863
|
+
]
|
|
864
|
+
view.delete()
|
|
865
|
+
|
|
866
|
+
def test_date_columns(self, client):
|
|
867
|
+
table = client.open_table("memory.superstore")
|
|
868
|
+
view = table.view(columns=["Order Date"])
|
|
869
|
+
json = view.to_json(start_row=0, end_row=5)
|
|
870
|
+
assert json == [
|
|
871
|
+
{"Order Date": 1478563200000},
|
|
872
|
+
{"Order Date": 1478563200000},
|
|
873
|
+
{"Order Date": 1465689600000},
|
|
874
|
+
{"Order Date": 1444521600000},
|
|
875
|
+
{"Order Date": 1444521600000},
|
|
876
|
+
]
|
|
877
|
+
view.delete()
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
class TestDuckDBCombinedOperations:
|
|
881
|
+
def test_group_by_filter_sort(self, client):
|
|
882
|
+
table = client.open_table("memory.superstore")
|
|
883
|
+
view = table.view(
|
|
884
|
+
columns=["Sales"],
|
|
885
|
+
group_by=["Category"],
|
|
886
|
+
filter=[["Region", "==", "West"]],
|
|
887
|
+
sort=[["Sales", "desc"]],
|
|
888
|
+
aggregates={"Sales": "sum"},
|
|
889
|
+
)
|
|
890
|
+
json = view.to_json()
|
|
891
|
+
assert json == [
|
|
892
|
+
{"__ROW_PATH__": [], "Sales": 725457.8245000006},
|
|
893
|
+
{"__ROW_PATH__": ["Furniture"], "Sales": 252612.7435000003},
|
|
894
|
+
{"__ROW_PATH__": ["Technology"], "Sales": 251991.83199999997},
|
|
895
|
+
{"__ROW_PATH__": ["Office Supplies"], "Sales": 220853.24900000007},
|
|
896
|
+
]
|
|
897
|
+
view.delete()
|
|
898
|
+
|
|
899
|
+
def test_split_by_group_by_filter(self, client):
|
|
900
|
+
table = client.open_table("memory.superstore")
|
|
901
|
+
view = table.view(
|
|
902
|
+
columns=["Sales"],
|
|
903
|
+
group_by=["Category"],
|
|
904
|
+
split_by=["Region"],
|
|
905
|
+
filter=[["Quantity", ">", 3]],
|
|
906
|
+
aggregates={"Sales": "sum"},
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
paths = joined_column_paths(view)
|
|
910
|
+
assert paths == [
|
|
911
|
+
"Central|Sales",
|
|
912
|
+
"East|Sales",
|
|
913
|
+
"South|Sales",
|
|
914
|
+
"West|Sales",
|
|
915
|
+
]
|
|
916
|
+
|
|
917
|
+
num_rows = view.num_rows()
|
|
918
|
+
assert num_rows == 4
|
|
919
|
+
|
|
920
|
+
json = view.to_json()
|
|
921
|
+
assert json == [
|
|
922
|
+
{
|
|
923
|
+
"__ROW_PATH__": [],
|
|
924
|
+
"Central|Sales": 332883.0567999998,
|
|
925
|
+
"East|Sales": 455143.735,
|
|
926
|
+
"South|Sales": 274208.7699999999,
|
|
927
|
+
"West|Sales": 470561.28350000136,
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
"__ROW_PATH__": ["Furniture"],
|
|
931
|
+
"Central|Sales": 111457.73279999988,
|
|
932
|
+
"East|Sales": 140376.95899999997,
|
|
933
|
+
"South|Sales": 80859.618,
|
|
934
|
+
"West|Sales": 165219.5734999998,
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
"__ROW_PATH__": ["Office Supplies"],
|
|
938
|
+
"Central|Sales": 103937.78599999992,
|
|
939
|
+
"East|Sales": 135823.893,
|
|
940
|
+
"South|Sales": 84393.3579999999,
|
|
941
|
+
"West|Sales": 140206.93099999975,
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
"__ROW_PATH__": ["Technology"],
|
|
945
|
+
"Central|Sales": 117487.53800000002,
|
|
946
|
+
"East|Sales": 178942.883,
|
|
947
|
+
"South|Sales": 108955.79400000005,
|
|
948
|
+
"West|Sales": 165134.77900000007,
|
|
949
|
+
},
|
|
950
|
+
]
|
|
951
|
+
view.delete()
|
|
952
|
+
|
|
953
|
+
def test_split_by_only(self, client):
|
|
954
|
+
table = client.open_table("memory.superstore")
|
|
955
|
+
view = table.view(
|
|
956
|
+
columns=["Sales"],
|
|
957
|
+
split_by=["Region"],
|
|
958
|
+
filter=[["Quantity", ">", 3]],
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
paths = joined_column_paths(view)
|
|
962
|
+
assert paths == [
|
|
963
|
+
"Central|Sales",
|
|
964
|
+
"East|Sales",
|
|
965
|
+
"South|Sales",
|
|
966
|
+
"West|Sales",
|
|
967
|
+
]
|
|
968
|
+
|
|
969
|
+
num_rows = view.num_rows()
|
|
970
|
+
assert num_rows == 4284
|
|
971
|
+
|
|
972
|
+
json = view.to_json(start_row=0, end_row=1)
|
|
973
|
+
assert json == [
|
|
974
|
+
{
|
|
975
|
+
"Central|Sales": None,
|
|
976
|
+
"East|Sales": None,
|
|
977
|
+
"South|Sales": 957.5775,
|
|
978
|
+
"West|Sales": None,
|
|
979
|
+
},
|
|
980
|
+
]
|
|
981
|
+
view.delete()
|
|
982
|
+
|
|
983
|
+
def test_expressions_group_by_sort(self, client):
|
|
984
|
+
table = client.open_table("memory.superstore")
|
|
985
|
+
view = table.view(
|
|
986
|
+
columns=["profitmargin"],
|
|
987
|
+
group_by=["Region"],
|
|
988
|
+
expressions={"profitmargin": '"Profit" / "Sales" * 100'},
|
|
989
|
+
sort=[["profitmargin", "desc"]],
|
|
990
|
+
aggregates={"profitmargin": "avg"},
|
|
991
|
+
)
|
|
992
|
+
json = view.to_json()
|
|
993
|
+
assert json == [
|
|
994
|
+
{"__ROW_PATH__": [], "profitmargin": 12.031392972104467},
|
|
995
|
+
{"__ROW_PATH__": ["West"], "profitmargin": 21.948661793784012},
|
|
996
|
+
{"__ROW_PATH__": ["East"], "profitmargin": 16.722695960406636},
|
|
997
|
+
{"__ROW_PATH__": ["South"], "profitmargin": 16.35190329218107},
|
|
998
|
+
{"__ROW_PATH__": ["Central"], "profitmargin": -10.407293926323575},
|
|
999
|
+
]
|
|
1000
|
+
view.delete()
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
class TestDuckDBMinMax:
|
|
1004
|
+
def test_min_max_integer(self, client):
|
|
1005
|
+
table = client.open_table("memory.superstore")
|
|
1006
|
+
view = table.view(columns=["Quantity"])
|
|
1007
|
+
min_val, max_val = view.get_min_max("Quantity")
|
|
1008
|
+
assert min_val == 1
|
|
1009
|
+
assert max_val == 14
|
|
1010
|
+
view.delete()
|
|
1011
|
+
|
|
1012
|
+
def test_min_max_float(self, client):
|
|
1013
|
+
table = client.open_table("memory.superstore")
|
|
1014
|
+
view = table.view(columns=["Sales"])
|
|
1015
|
+
min_val, max_val = view.get_min_max("Sales")
|
|
1016
|
+
assert min_val == 0.444
|
|
1017
|
+
assert max_val == 22638.48
|
|
1018
|
+
view.delete()
|
|
1019
|
+
|
|
1020
|
+
def test_min_max_string(self, client):
|
|
1021
|
+
table = client.open_table("memory.superstore")
|
|
1022
|
+
view = table.view(columns=["Category"])
|
|
1023
|
+
min_val, max_val = view.get_min_max("Category")
|
|
1024
|
+
assert min_val == "Furniture"
|
|
1025
|
+
assert max_val == "Technology"
|
|
1026
|
+
view.delete()
|
|
1027
|
+
|
|
1028
|
+
def test_min_max_with_group_by(self, client):
|
|
1029
|
+
table = client.open_table("memory.superstore")
|
|
1030
|
+
view = table.view(
|
|
1031
|
+
columns=["Sales"],
|
|
1032
|
+
group_by=["Region"],
|
|
1033
|
+
aggregates={"Sales": "sum"},
|
|
1034
|
+
)
|
|
1035
|
+
min_val, max_val = view.get_min_max("Sales")
|
|
1036
|
+
assert min_val > 0
|
|
1037
|
+
assert max_val > 0
|
|
1038
|
+
assert max_val >= min_val
|
|
1039
|
+
view.delete()
|
|
1040
|
+
|
|
1041
|
+
def test_min_max_with_filter(self, client):
|
|
1042
|
+
table = client.open_table("memory.superstore")
|
|
1043
|
+
view = table.view(
|
|
1044
|
+
columns=["Quantity"],
|
|
1045
|
+
filter=[["Quantity", ">", 10]],
|
|
1046
|
+
)
|
|
1047
|
+
min_val, max_val = view.get_min_max("Quantity")
|
|
1048
|
+
assert min_val >= 11
|
|
1049
|
+
assert max_val == 14
|
|
1050
|
+
view.delete()
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
class TestDuckDBCoerceTypes:
|
|
1054
|
+
"""The DuckDB types whose Arrow is not already one of Perspective's."""
|
|
1055
|
+
|
|
1056
|
+
def test_schema(self, client):
|
|
1057
|
+
table = client.open_table("memory.coerce_types")
|
|
1058
|
+
assert table.schema() == {
|
|
1059
|
+
"tiny": "integer",
|
|
1060
|
+
"small": "integer",
|
|
1061
|
+
"utiny": "integer",
|
|
1062
|
+
"usmall": "integer",
|
|
1063
|
+
"uint": "float",
|
|
1064
|
+
"ubig": "float",
|
|
1065
|
+
"big": "float",
|
|
1066
|
+
"float": "float",
|
|
1067
|
+
"decimal": "float",
|
|
1068
|
+
"time": "datetime",
|
|
1069
|
+
"timestamp": "datetime",
|
|
1070
|
+
"date": "date",
|
|
1071
|
+
"enum": "string",
|
|
1072
|
+
"string": "string",
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
def test_narrow_integers_flat(self, client):
|
|
1076
|
+
table = client.open_table("memory.coerce_types")
|
|
1077
|
+
view = table.view(columns=["tiny", "small", "utiny", "usmall"])
|
|
1078
|
+
assert view.to_json() == [
|
|
1079
|
+
{"tiny": -1, "small": -300, "utiny": 255, "usmall": 65535},
|
|
1080
|
+
{"tiny": 1, "small": 300, "utiny": 0, "usmall": 0},
|
|
1081
|
+
]
|
|
1082
|
+
view.delete()
|
|
1083
|
+
|
|
1084
|
+
def test_wide_and_fractional_numbers_flat(self, client):
|
|
1085
|
+
table = client.open_table("memory.coerce_types")
|
|
1086
|
+
view = table.view(columns=["uint", "ubig", "big", "float", "decimal"])
|
|
1087
|
+
assert view.to_json() == [
|
|
1088
|
+
{
|
|
1089
|
+
"uint": 4294967295.0,
|
|
1090
|
+
"ubig": 9007199254740992.0,
|
|
1091
|
+
"big": 9007199254740992.0,
|
|
1092
|
+
"float": 1.5,
|
|
1093
|
+
"decimal": pytest.approx(1.234),
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
"uint": 0.0,
|
|
1097
|
+
"ubig": 0.0,
|
|
1098
|
+
"big": -9007199254740992.0,
|
|
1099
|
+
"float": -1.5,
|
|
1100
|
+
"decimal": pytest.approx(-5.678),
|
|
1101
|
+
},
|
|
1102
|
+
]
|
|
1103
|
+
view.delete()
|
|
1104
|
+
|
|
1105
|
+
def test_temporal_flat(self, client):
|
|
1106
|
+
table = client.open_table("memory.coerce_types")
|
|
1107
|
+
view = table.view(columns=["time", "timestamp", "date"])
|
|
1108
|
+
assert view.to_json() == [
|
|
1109
|
+
{
|
|
1110
|
+
"time": 3661000,
|
|
1111
|
+
"timestamp": 1672531200000,
|
|
1112
|
+
"date": 1672531200000,
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
"time": 1000,
|
|
1116
|
+
"timestamp": 1672617600000,
|
|
1117
|
+
"date": 1672617600000,
|
|
1118
|
+
},
|
|
1119
|
+
]
|
|
1120
|
+
view.delete()
|
|
1121
|
+
|
|
1122
|
+
def test_enum_flat(self, client):
|
|
1123
|
+
# Dictionary-encoded by DuckDB, with a key width chosen from the
|
|
1124
|
+
# `ENUM`'s cardinality rather than the `Int32` Perspective uses.
|
|
1125
|
+
table = client.open_table("memory.coerce_types")
|
|
1126
|
+
view = table.view(columns=["enum", "string"])
|
|
1127
|
+
assert view.to_json() == [
|
|
1128
|
+
{"enum": "happy", "string": "a"},
|
|
1129
|
+
{"enum": "sad", "string": "b"},
|
|
1130
|
+
]
|
|
1131
|
+
view.delete()
|
|
1132
|
+
|
|
1133
|
+
def test_enum_group_by(self, client):
|
|
1134
|
+
# The #3149 repro: the row path is read out of that dictionary.
|
|
1135
|
+
table = client.open_table("memory.coerce_types")
|
|
1136
|
+
view = table.view(
|
|
1137
|
+
group_by=["enum"],
|
|
1138
|
+
columns=["tiny"],
|
|
1139
|
+
aggregates={"tiny": "sum"},
|
|
1140
|
+
)
|
|
1141
|
+
assert view.to_json() == [
|
|
1142
|
+
{"__ROW_PATH__": [], "tiny": 0},
|
|
1143
|
+
{"__ROW_PATH__": ["happy"], "tiny": -1},
|
|
1144
|
+
{"__ROW_PATH__": ["sad"], "tiny": 1},
|
|
1145
|
+
]
|
|
1146
|
+
view.delete()
|
|
1147
|
+
|
|
1148
|
+
def test_decimal_group_by(self, client):
|
|
1149
|
+
table = client.open_table("memory.coerce_types")
|
|
1150
|
+
view = table.view(
|
|
1151
|
+
group_by=["decimal"],
|
|
1152
|
+
columns=["tiny"],
|
|
1153
|
+
aggregates={"tiny": "sum"},
|
|
1154
|
+
)
|
|
1155
|
+
assert view.to_json() == [
|
|
1156
|
+
{"__ROW_PATH__": [], "tiny": 0},
|
|
1157
|
+
{"__ROW_PATH__": [pytest.approx(-5.678)], "tiny": 1},
|
|
1158
|
+
{"__ROW_PATH__": [pytest.approx(1.234)], "tiny": -1},
|
|
1159
|
+
]
|
|
1160
|
+
view.delete()
|
|
1161
|
+
|
|
1162
|
+
def test_column_values_view(self, client):
|
|
1163
|
+
# The filter dropdown's query shape - group by the column, select
|
|
1164
|
+
# no columns at all.
|
|
1165
|
+
table = client.open_table("memory.coerce_types")
|
|
1166
|
+
view = table.view(group_by=["enum"], columns=[])
|
|
1167
|
+
csv = view.to_csv()
|
|
1168
|
+
assert [line for line in csv.splitlines() if line] == [
|
|
1169
|
+
"enum",
|
|
1170
|
+
"null",
|
|
1171
|
+
'"happy"',
|
|
1172
|
+
'"sad"',
|
|
1173
|
+
]
|
|
1174
|
+
view.delete()
|
|
1175
|
+
|
|
1176
|
+
def test_filter_matching_nothing(self, client):
|
|
1177
|
+
# DuckDB returns a chunkless table for an empty result, which
|
|
1178
|
+
# `write_table` serializes as a schema with no record batches at
|
|
1179
|
+
# all. That is a view with no rows, not a broken stream.
|
|
1180
|
+
table = client.open_table("memory.coerce_types")
|
|
1181
|
+
view = table.view(
|
|
1182
|
+
columns=["tiny"],
|
|
1183
|
+
filter=[["string", "==", "no such value"]],
|
|
1184
|
+
)
|
|
1185
|
+
assert view.to_json() == []
|
|
1186
|
+
assert view.to_columns() == {"tiny": []}
|
|
1187
|
+
view.delete()
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
class TestDuckDBTableFromView:
|
|
1191
|
+
def test_group_by_view_unrolls_row_path(self, client):
|
|
1192
|
+
table = client.open_table("memory.superstore")
|
|
1193
|
+
view = table.view(
|
|
1194
|
+
columns=["Sales"], group_by=["Region"], aggregates={"Sales": "sum"}
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
derived = client.table(view, name="py_derived_group")
|
|
1198
|
+
assert derived.schema() == {
|
|
1199
|
+
"Region": "string",
|
|
1200
|
+
"Sales": "float",
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
child = derived.view(columns=["Region", "Sales"])
|
|
1204
|
+
expected = sorted(
|
|
1205
|
+
(str(row["__ROW_PATH__"][0]) if row["__ROW_PATH__"] else "None", row["Sales"])
|
|
1206
|
+
for row in view.to_json()
|
|
1207
|
+
)
|
|
1208
|
+
|
|
1209
|
+
actual = sorted(
|
|
1210
|
+
(str(row["Region"]), row["Sales"]) for row in child.to_json()
|
|
1211
|
+
)
|
|
1212
|
+
|
|
1213
|
+
assert actual == expected
|
|
1214
|
+
child.delete()
|
|
1215
|
+
derived.delete()
|
|
1216
|
+
view.delete()
|
|
1217
|
+
|
|
1218
|
+
def test_group_by_view_qualifies_key_aggregate(self, client):
|
|
1219
|
+
table = client.open_table("memory.superstore")
|
|
1220
|
+
view = table.view(
|
|
1221
|
+
columns=["Region", "Sales"],
|
|
1222
|
+
group_by=["Region"],
|
|
1223
|
+
aggregates={"Region": "count", "Sales": "sum"},
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
derived = client.table(view, name="py_derived_qualified")
|
|
1227
|
+
assert derived.columns() == ["Region", "Region (count)", "Sales"]
|
|
1228
|
+
child = derived.view(columns=["Region", "Region (count)"])
|
|
1229
|
+
assert child.num_rows() == view.num_rows()
|
|
1230
|
+
child.delete()
|
|
1231
|
+
derived.delete()
|
|
1232
|
+
view.delete()
|
|
1233
|
+
|
|
1234
|
+
def test_explicit_schema(self, client):
|
|
1235
|
+
table = client.open_table("memory.superstore")
|
|
1236
|
+
view = table.view(
|
|
1237
|
+
columns=["Sales"],
|
|
1238
|
+
group_by=["Category"],
|
|
1239
|
+
split_by=["Region"],
|
|
1240
|
+
aggregates={"Sales": "sum"},
|
|
1241
|
+
group_rollup_mode="flat",
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
derived = client.table(
|
|
1245
|
+
view,
|
|
1246
|
+
name="py_derived_schema",
|
|
1247
|
+
schema={
|
|
1248
|
+
"Category": "string",
|
|
1249
|
+
"West|Sales": "float",
|
|
1250
|
+
"North|Sales": "float",
|
|
1251
|
+
},
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
assert derived.columns() == [
|
|
1255
|
+
"Category",
|
|
1256
|
+
"West|Sales",
|
|
1257
|
+
"North|Sales",
|
|
1258
|
+
]
|
|
1259
|
+
|
|
1260
|
+
child = derived.view(
|
|
1261
|
+
columns=["Category", "West|Sales", "North|Sales"]
|
|
1262
|
+
)
|
|
1263
|
+
|
|
1264
|
+
rows = child.to_json()
|
|
1265
|
+
assert len(rows) == 3
|
|
1266
|
+
assert all(row["North|Sales"] is None for row in rows)
|
|
1267
|
+
child.delete()
|
|
1268
|
+
derived.delete()
|
|
1269
|
+
view.delete()
|
|
1270
|
+
|
|
1271
|
+
def test_rejects_mismatched_type(self, client):
|
|
1272
|
+
table = client.open_table("memory.superstore")
|
|
1273
|
+
view = table.view(
|
|
1274
|
+
columns=["Sales"], group_by=["Region"], aggregates={"Sales": "sum"}
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
with pytest.raises(Exception):
|
|
1278
|
+
client.table(view, schema={"Sales": "string"})
|
|
1279
|
+
|
|
1280
|
+
view.delete()
|