singlestoredb 0.4.0__py3-none-any.whl → 1.0.4__py3-none-any.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.
Potentially problematic release.
This version of singlestoredb might be problematic. Click here for more details.
- singlestoredb/__init__.py +33 -1
- singlestoredb/alchemy/__init__.py +90 -0
- singlestoredb/auth.py +5 -1
- singlestoredb/config.py +116 -14
- singlestoredb/connection.py +483 -516
- singlestoredb/converters.py +238 -135
- singlestoredb/exceptions.py +30 -2
- singlestoredb/functions/__init__.py +1 -0
- singlestoredb/functions/decorator.py +142 -0
- singlestoredb/functions/dtypes.py +1639 -0
- singlestoredb/functions/ext/__init__.py +2 -0
- singlestoredb/functions/ext/arrow.py +375 -0
- singlestoredb/functions/ext/asgi.py +661 -0
- singlestoredb/functions/ext/json.py +427 -0
- singlestoredb/functions/ext/mmap.py +306 -0
- singlestoredb/functions/ext/rowdat_1.py +744 -0
- singlestoredb/functions/signature.py +673 -0
- singlestoredb/fusion/__init__.py +11 -0
- singlestoredb/fusion/graphql.py +213 -0
- singlestoredb/fusion/handler.py +621 -0
- singlestoredb/fusion/handlers/stage.py +257 -0
- singlestoredb/fusion/handlers/utils.py +162 -0
- singlestoredb/fusion/handlers/workspace.py +412 -0
- singlestoredb/fusion/registry.py +164 -0
- singlestoredb/fusion/result.py +399 -0
- singlestoredb/http/__init__.py +27 -0
- singlestoredb/{http.py → http/connection.py} +555 -154
- singlestoredb/management/__init__.py +3 -0
- singlestoredb/management/billing_usage.py +148 -0
- singlestoredb/management/cluster.py +14 -6
- singlestoredb/management/manager.py +100 -38
- singlestoredb/management/organization.py +188 -0
- singlestoredb/management/region.py +5 -5
- singlestoredb/management/utils.py +281 -2
- singlestoredb/management/workspace.py +1344 -49
- singlestoredb/{clients/pymysqlsv → mysql}/__init__.py +16 -21
- singlestoredb/{clients/pymysqlsv → mysql}/_auth.py +39 -8
- singlestoredb/{clients/pymysqlsv → mysql}/charset.py +26 -23
- singlestoredb/{clients/pymysqlsv/connections.py → mysql/connection.py} +532 -165
- singlestoredb/{clients/pymysqlsv → mysql}/constants/CLIENT.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/COMMAND.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/CR.py +0 -2
- singlestoredb/{clients/pymysqlsv → mysql}/constants/ER.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/FIELD_TYPE.py +1 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/FLAG.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/SERVER_STATUS.py +0 -1
- singlestoredb/mysql/converters.py +271 -0
- singlestoredb/{clients/pymysqlsv → mysql}/cursors.py +228 -112
- singlestoredb/mysql/err.py +92 -0
- singlestoredb/{clients/pymysqlsv → mysql}/optionfile.py +5 -4
- singlestoredb/{clients/pymysqlsv → mysql}/protocol.py +49 -20
- singlestoredb/mysql/tests/__init__.py +19 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/base.py +32 -12
- singlestoredb/mysql/tests/conftest.py +37 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_DictCursor.py +11 -7
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_SSCursor.py +17 -12
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_basic.py +32 -24
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_connection.py +130 -119
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_converters.py +9 -7
- singlestoredb/mysql/tests/test_cursor.py +141 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_err.py +3 -2
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_issues.py +35 -27
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_load_local.py +13 -11
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_nextset.py +7 -3
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_optionfile.py +2 -1
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/__init__.py +1 -1
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/capabilities.py +19 -17
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/dbapi20.py +31 -22
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +3 -4
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +24 -20
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +4 -4
- singlestoredb/{clients/pymysqlsv → mysql}/times.py +3 -4
- singlestoredb/pytest.py +283 -0
- singlestoredb/tests/empty.sql +0 -0
- singlestoredb/tests/ext_funcs/__init__.py +385 -0
- singlestoredb/tests/test.sql +210 -0
- singlestoredb/tests/test2.sql +1 -0
- singlestoredb/tests/test_basics.py +482 -115
- singlestoredb/tests/test_config.py +13 -13
- singlestoredb/tests/test_connection.py +241 -305
- singlestoredb/tests/test_dbapi.py +27 -0
- singlestoredb/tests/test_ext_func.py +1193 -0
- singlestoredb/tests/test_ext_func_data.py +1101 -0
- singlestoredb/tests/test_fusion.py +465 -0
- singlestoredb/tests/test_http.py +32 -26
- singlestoredb/tests/test_management.py +588 -8
- singlestoredb/tests/test_plugin.py +33 -0
- singlestoredb/tests/test_results.py +11 -12
- singlestoredb/tests/test_udf.py +687 -0
- singlestoredb/tests/utils.py +3 -2
- singlestoredb/utils/config.py +58 -0
- singlestoredb/utils/debug.py +13 -0
- singlestoredb/utils/mogrify.py +151 -0
- singlestoredb/utils/results.py +4 -1
- singlestoredb-1.0.4.dist-info/METADATA +139 -0
- singlestoredb-1.0.4.dist-info/RECORD +112 -0
- {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/WHEEL +1 -1
- singlestoredb-1.0.4.dist-info/entry_points.txt +2 -0
- singlestoredb/clients/pymysqlsv/converters.py +0 -365
- singlestoredb/clients/pymysqlsv/err.py +0 -144
- singlestoredb/clients/pymysqlsv/tests/__init__.py +0 -19
- singlestoredb/clients/pymysqlsv/tests/test_cursor.py +0 -133
- singlestoredb/clients/pymysqlsv/tests/thirdparty/test_MySQLdb/__init__.py +0 -9
- singlestoredb/drivers/__init__.py +0 -45
- singlestoredb/drivers/base.py +0 -198
- singlestoredb/drivers/cymysql.py +0 -38
- singlestoredb/drivers/http.py +0 -47
- singlestoredb/drivers/mariadb.py +0 -40
- singlestoredb/drivers/mysqlconnector.py +0 -49
- singlestoredb/drivers/mysqldb.py +0 -60
- singlestoredb/drivers/pymysql.py +0 -37
- singlestoredb/drivers/pymysqlsv.py +0 -35
- singlestoredb/drivers/pyodbc.py +0 -65
- singlestoredb-0.4.0.dist-info/METADATA +0 -111
- singlestoredb-0.4.0.dist-info/RECORD +0 -86
- /singlestoredb/{clients → fusion/handlers}/__init__.py +0 -0
- /singlestoredb/{clients/pymysqlsv → mysql}/constants/__init__.py +0 -0
- {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/LICENSE +0 -0
- {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import Dict
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .. import result
|
|
8
|
+
from ..handler import SQLHandler
|
|
9
|
+
from ..result import FusionSQLResult
|
|
10
|
+
from .utils import dt_isoformat
|
|
11
|
+
from .utils import get_workspace
|
|
12
|
+
from .utils import get_workspace_group
|
|
13
|
+
from .utils import get_workspace_manager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ShowRegionsHandler(SQLHandler):
|
|
17
|
+
"""
|
|
18
|
+
SHOW REGIONS [ <like> ] [ <order-by> ] [ <limit> ];
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
|
|
23
|
+
manager = get_workspace_manager()
|
|
24
|
+
|
|
25
|
+
res = FusionSQLResult()
|
|
26
|
+
res.add_field('Name', result.STRING)
|
|
27
|
+
res.add_field('ID', result.STRING)
|
|
28
|
+
res.add_field('Provider', result.STRING)
|
|
29
|
+
|
|
30
|
+
res.set_rows([(x.name, x.id, x.provider) for x in manager.regions])
|
|
31
|
+
|
|
32
|
+
if params['like']:
|
|
33
|
+
res = res.like(Name=params['like'])
|
|
34
|
+
|
|
35
|
+
return res.order_by(**params['order_by']).limit(params['limit'])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
ShowRegionsHandler.register(overwrite=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ShowWorkspaceGroupsHandler(SQLHandler):
|
|
42
|
+
"""
|
|
43
|
+
SHOW WORKSPACE GROUPS [ <like> ] [ <extended> ] [ <order-by> ] [ <limit> ];
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
|
|
48
|
+
manager = get_workspace_manager()
|
|
49
|
+
|
|
50
|
+
res = FusionSQLResult()
|
|
51
|
+
res.add_field('Name', result.STRING)
|
|
52
|
+
res.add_field('ID', result.STRING)
|
|
53
|
+
res.add_field('Region', result.STRING)
|
|
54
|
+
res.add_field('FirewallRanges', result.JSON)
|
|
55
|
+
|
|
56
|
+
if params['extended']:
|
|
57
|
+
res.add_field('CreatedAt', result.DATETIME)
|
|
58
|
+
res.add_field('TerminatedAt', result.DATETIME)
|
|
59
|
+
|
|
60
|
+
def fields(x: Any) -> Any:
|
|
61
|
+
return (
|
|
62
|
+
x.name, x.id, x.region.name,
|
|
63
|
+
json.dumps(x.firewall_ranges),
|
|
64
|
+
dt_isoformat(x.created_at),
|
|
65
|
+
dt_isoformat(x.terminated_at),
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
def fields(x: Any) -> Any:
|
|
69
|
+
return (x.name, x.id, x.region.name, json.dumps(x.firewall_ranges))
|
|
70
|
+
|
|
71
|
+
res.set_rows([fields(x) for x in manager.workspace_groups])
|
|
72
|
+
|
|
73
|
+
if params['like']:
|
|
74
|
+
res = res.like(Name=params['like'])
|
|
75
|
+
|
|
76
|
+
return res.order_by(**params['order_by']).limit(params['limit'])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
ShowWorkspaceGroupsHandler.register(overwrite=True)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ShowWorkspacesHandler(SQLHandler):
|
|
83
|
+
"""
|
|
84
|
+
SHOW WORKSPACES [ in_group ] [ <like> ] [ <extended> ] [ <order-by> ] [ <limit> ];
|
|
85
|
+
|
|
86
|
+
# Workspace group
|
|
87
|
+
in_group = IN GROUP { group_id | group_name }
|
|
88
|
+
|
|
89
|
+
# ID of group
|
|
90
|
+
group_id = ID '<group-id>'
|
|
91
|
+
|
|
92
|
+
# Name of group
|
|
93
|
+
group_name = '<group-name>'
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
|
|
98
|
+
res = FusionSQLResult()
|
|
99
|
+
res.add_field('Name', result.STRING)
|
|
100
|
+
res.add_field('ID', result.STRING)
|
|
101
|
+
res.add_field('Size', result.STRING)
|
|
102
|
+
res.add_field('State', result.STRING)
|
|
103
|
+
|
|
104
|
+
workspace_group = get_workspace_group(params)
|
|
105
|
+
|
|
106
|
+
if params['extended']:
|
|
107
|
+
res.add_field('Endpoint', result.STRING)
|
|
108
|
+
res.add_field('CreatedAt', result.DATETIME)
|
|
109
|
+
res.add_field('TerminatedAt', result.DATETIME)
|
|
110
|
+
|
|
111
|
+
def fields(x: Any) -> Any:
|
|
112
|
+
return (
|
|
113
|
+
x.name, x.id, x.size, x.state,
|
|
114
|
+
x.endpoint, dt_isoformat(x.created_at),
|
|
115
|
+
dt_isoformat(x.terminated_at),
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
def fields(x: Any) -> Any:
|
|
119
|
+
return (x.name, x.id, x.size, x.state)
|
|
120
|
+
|
|
121
|
+
res.set_rows([fields(x) for x in workspace_group.workspaces])
|
|
122
|
+
|
|
123
|
+
if params['like']:
|
|
124
|
+
res = res.like(Name=params['like'])
|
|
125
|
+
|
|
126
|
+
return res.order_by(**params['order_by']).limit(params['limit'])
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
ShowWorkspacesHandler.register(overwrite=True)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class CreateWorkspaceGroupHandler(SQLHandler):
|
|
133
|
+
"""
|
|
134
|
+
CREATE WORKSPACE GROUP [ if_not_exists ] group_name
|
|
135
|
+
IN REGION { region_id | region_name }
|
|
136
|
+
[ with_password ]
|
|
137
|
+
[ expires_at ]
|
|
138
|
+
[ with_firewall_ranges ]
|
|
139
|
+
;
|
|
140
|
+
|
|
141
|
+
# Only create workspace group if it doesn't exist already
|
|
142
|
+
if_not_exists = IF NOT EXISTS
|
|
143
|
+
|
|
144
|
+
# Name of the workspace group
|
|
145
|
+
group_name = '<group-name>'
|
|
146
|
+
|
|
147
|
+
# ID of region to create workspace group in
|
|
148
|
+
region_id = ID '<region-id>'
|
|
149
|
+
|
|
150
|
+
# Name of region to create workspace group in
|
|
151
|
+
region_name = '<region-name>'
|
|
152
|
+
|
|
153
|
+
# Admin password
|
|
154
|
+
with_password = WITH PASSWORD '<password>'
|
|
155
|
+
|
|
156
|
+
# Datetime or interval for expiration date/time of workspace group
|
|
157
|
+
expires_at = EXPIRES AT '<iso-datetime-or-interval>'
|
|
158
|
+
|
|
159
|
+
# Incoming IP ranges
|
|
160
|
+
with_firewall_ranges = WITH FIREWALL RANGES '<ip-range>',...
|
|
161
|
+
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
|
|
165
|
+
manager = get_workspace_manager()
|
|
166
|
+
|
|
167
|
+
# Only create if one doesn't exist
|
|
168
|
+
if params['if_not_exists']:
|
|
169
|
+
try:
|
|
170
|
+
get_workspace_group(params)
|
|
171
|
+
return None
|
|
172
|
+
except (ValueError, KeyError):
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
# Get region ID
|
|
176
|
+
if params['region_name']:
|
|
177
|
+
regs = [x for x in manager.regions if x.name == params['region_name']]
|
|
178
|
+
if not regs:
|
|
179
|
+
raise KeyError(f'no region found with name "{params["region_name"]}"')
|
|
180
|
+
if len(regs) > 1:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
f'multiple regions found with the name "{params["region_name"]}"',
|
|
183
|
+
)
|
|
184
|
+
region_id = regs[0].id
|
|
185
|
+
else:
|
|
186
|
+
region_id = params['region_id']
|
|
187
|
+
|
|
188
|
+
manager.create_workspace_group(
|
|
189
|
+
params['group_name'],
|
|
190
|
+
region=region_id,
|
|
191
|
+
admin_password=params['with_password'],
|
|
192
|
+
expires_at=params['expires_at'],
|
|
193
|
+
firewall_ranges=params['with_firewall_ranges'],
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
CreateWorkspaceGroupHandler.register(overwrite=True)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class CreateWorkspaceHandler(SQLHandler):
|
|
203
|
+
"""
|
|
204
|
+
CREATE WORKSPACE [ if_not_exists ] workspace_name [ in_group ]
|
|
205
|
+
WITH SIZE size [ wait_on_active ];
|
|
206
|
+
|
|
207
|
+
# Create workspace in workspace group
|
|
208
|
+
in_group = IN GROUP { group_id | group_name }
|
|
209
|
+
|
|
210
|
+
# Only run command if workspace doesn't already exist
|
|
211
|
+
if_not_exists = IF NOT EXISTS
|
|
212
|
+
|
|
213
|
+
# Name of the workspace
|
|
214
|
+
workspace_name = '<workspace-name>'
|
|
215
|
+
|
|
216
|
+
# ID of the group to create workspace in
|
|
217
|
+
group_id = ID '<group-id>'
|
|
218
|
+
|
|
219
|
+
# Name of the group to create workspace in
|
|
220
|
+
group_name = '<group-name>'
|
|
221
|
+
|
|
222
|
+
# Runtime size
|
|
223
|
+
size = '<size>'
|
|
224
|
+
|
|
225
|
+
# Wait for workspace to be active before continuing
|
|
226
|
+
wait_on_active = WAIT ON ACTIVE
|
|
227
|
+
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
|
|
231
|
+
workspace_group = get_workspace_group(params)
|
|
232
|
+
|
|
233
|
+
# Only create if one doesn't exist
|
|
234
|
+
if params['if_not_exists']:
|
|
235
|
+
try:
|
|
236
|
+
workspace_group.workspaces[params['workspace_name']]
|
|
237
|
+
return None
|
|
238
|
+
except KeyError:
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
workspace_group.create_workspace(
|
|
242
|
+
params['workspace_name'], size=params['size'],
|
|
243
|
+
wait_on_active=params['wait_on_active'],
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
CreateWorkspaceHandler.register(overwrite=True)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class SuspendWorkspaceHandler(SQLHandler):
|
|
253
|
+
"""
|
|
254
|
+
SUSPEND WORKSPACE workspace [ in_group ] [ wait_on_suspended ];
|
|
255
|
+
|
|
256
|
+
# Workspace
|
|
257
|
+
workspace = { workspace_id | workspace_name }
|
|
258
|
+
|
|
259
|
+
# ID of workspace
|
|
260
|
+
workspace_id = ID '<workspace-id>'
|
|
261
|
+
|
|
262
|
+
# Name of workspace
|
|
263
|
+
workspace_name = '<workspace-name>'
|
|
264
|
+
|
|
265
|
+
# Workspace group
|
|
266
|
+
in_group = IN GROUP { group_id | group_name }
|
|
267
|
+
|
|
268
|
+
# ID of workspace group
|
|
269
|
+
group_id = ID '<group-id>'
|
|
270
|
+
|
|
271
|
+
# Name of workspace group
|
|
272
|
+
group_name = '<group-name>'
|
|
273
|
+
|
|
274
|
+
# Wait for workspace to be suspended before continuing
|
|
275
|
+
wait_on_suspended = WAIT ON SUSPENDED
|
|
276
|
+
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
|
|
280
|
+
ws = get_workspace(params)
|
|
281
|
+
ws.suspend(wait_on_suspended=params['wait_on_suspended'])
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
SuspendWorkspaceHandler.register(overwrite=True)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class ResumeWorkspaceHandler(SQLHandler):
|
|
289
|
+
"""
|
|
290
|
+
RESUME WORKSPACE workspace [ in_group ] [ wait_on_resumed ];
|
|
291
|
+
|
|
292
|
+
# Workspace
|
|
293
|
+
workspace = { workspace_id | workspace_name }
|
|
294
|
+
|
|
295
|
+
# ID of workspace
|
|
296
|
+
workspace_id = ID '<workspace-id>'
|
|
297
|
+
|
|
298
|
+
# Name of workspace
|
|
299
|
+
workspace_name = '<workspace-name>'
|
|
300
|
+
|
|
301
|
+
# Workspace group
|
|
302
|
+
in_group = IN GROUP { group_id | group_name }
|
|
303
|
+
|
|
304
|
+
# ID of workspace group
|
|
305
|
+
group_id = ID '<group-id>'
|
|
306
|
+
|
|
307
|
+
# Name of workspace group
|
|
308
|
+
group_name = '<group-name>'
|
|
309
|
+
|
|
310
|
+
# Wait for workspace to be resumed before continuing
|
|
311
|
+
wait_on_resumed = WAIT ON RESUMED
|
|
312
|
+
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
|
|
316
|
+
ws = get_workspace(params)
|
|
317
|
+
ws.resume(wait_on_resumed=params['wait_on_resumed'])
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
ResumeWorkspaceHandler.register(overwrite=True)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class DropWorkspaceGroupHandler(SQLHandler):
|
|
325
|
+
"""
|
|
326
|
+
DROP WORKSPACE GROUP [ if_exists ] group [ wait_on_terminated ] [ force ];
|
|
327
|
+
|
|
328
|
+
# Only run command if the workspace group exists
|
|
329
|
+
if_exists = IF EXISTS
|
|
330
|
+
|
|
331
|
+
# Workspace group
|
|
332
|
+
group = { group_id | group_name }
|
|
333
|
+
|
|
334
|
+
# ID of the workspace group to delete
|
|
335
|
+
group_id = ID '<group-id>'
|
|
336
|
+
|
|
337
|
+
# Name of the workspace group to delete
|
|
338
|
+
group_name = '<group-name>'
|
|
339
|
+
|
|
340
|
+
# Wait for termination to complete before continuing
|
|
341
|
+
wait_on_terminated = WAIT ON TERMINATED
|
|
342
|
+
|
|
343
|
+
# Should the workspace group be terminated even if it has workspaces?
|
|
344
|
+
force = FORCE
|
|
345
|
+
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
|
|
349
|
+
try:
|
|
350
|
+
workspace_group = get_workspace_group(params)
|
|
351
|
+
if workspace_group.terminated_at is not None:
|
|
352
|
+
raise KeyError('workspace group is alread terminated')
|
|
353
|
+
workspace_group.terminate(
|
|
354
|
+
wait_on_terminated=params['wait_on_terminated'],
|
|
355
|
+
force=params['force'],
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
except KeyError:
|
|
359
|
+
if not params['if_exists']:
|
|
360
|
+
raise
|
|
361
|
+
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
DropWorkspaceGroupHandler.register(overwrite=True)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class DropWorkspaceHandler(SQLHandler):
|
|
369
|
+
"""
|
|
370
|
+
DROP WORKSPACE [ if_exists ] workspace [ in_group ] [ wait_on_terminated ];
|
|
371
|
+
|
|
372
|
+
# Only drop workspace if it exists
|
|
373
|
+
if_exists = IF EXISTS
|
|
374
|
+
|
|
375
|
+
# Workspace
|
|
376
|
+
workspace = { workspace_id | workspace_name }
|
|
377
|
+
|
|
378
|
+
# ID of workspace
|
|
379
|
+
workspace_id = ID '<workspace-id>'
|
|
380
|
+
|
|
381
|
+
# Name of workspace
|
|
382
|
+
workspace_name = '<workspace-name>'
|
|
383
|
+
|
|
384
|
+
# Workspace group
|
|
385
|
+
in_group = IN GROUP { group_id | group_name }
|
|
386
|
+
|
|
387
|
+
# ID of workspace group
|
|
388
|
+
group_id = ID '<group-id>'
|
|
389
|
+
|
|
390
|
+
# Name of workspace group
|
|
391
|
+
group_name = '<group-name>'
|
|
392
|
+
|
|
393
|
+
# Wait for workspace to be terminated before continuing
|
|
394
|
+
wait_on_terminated = WAIT ON TERMINATED
|
|
395
|
+
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
def run(self, params: Dict[str, Any]) -> Optional[FusionSQLResult]:
|
|
399
|
+
try:
|
|
400
|
+
ws = get_workspace(params)
|
|
401
|
+
if ws.terminated_at is not None:
|
|
402
|
+
raise KeyError('workspace is already terminated')
|
|
403
|
+
ws.terminate(wait_on_terminated=params['wait_on_terminated'])
|
|
404
|
+
|
|
405
|
+
except KeyError:
|
|
406
|
+
if not params['if_exists']:
|
|
407
|
+
raise
|
|
408
|
+
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
DropWorkspaceHandler.register(overwrite=True)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import Dict
|
|
5
|
+
from typing import List
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from typing import Tuple
|
|
8
|
+
from typing import Type
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
from . import result
|
|
12
|
+
from .. import connection
|
|
13
|
+
from ..config import get_option
|
|
14
|
+
from .handler import SQLHandler
|
|
15
|
+
|
|
16
|
+
_handlers: Dict[str, Type[SQLHandler]] = {}
|
|
17
|
+
_handlers_re: Optional[Any] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def register_handler(handler: Type[SQLHandler], overwrite: bool = False) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Register a new SQL handler.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
handler : SQLHandler subclass
|
|
27
|
+
The handler class to register
|
|
28
|
+
overwrite : bool, optional
|
|
29
|
+
Should an existing handler be overwritten if it uses the same command key?
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
global _handlers
|
|
33
|
+
global _handlers_re
|
|
34
|
+
|
|
35
|
+
# Build key for handler
|
|
36
|
+
key = ' '.join(x.upper() for x in handler.command_key)
|
|
37
|
+
|
|
38
|
+
# Check for existing handler with same key
|
|
39
|
+
if not overwrite and key in _handlers:
|
|
40
|
+
raise ValueError(f'command already exists, use overwrite=True to override: {key}')
|
|
41
|
+
|
|
42
|
+
# Add handler to registry
|
|
43
|
+
_handlers[key] = handler
|
|
44
|
+
|
|
45
|
+
# Build regex to detect fusion query
|
|
46
|
+
keys = sorted(_handlers.keys(), key=lambda x: (-len(x), x))
|
|
47
|
+
keys_str = '|'.join(x.replace(' ', '\\s+') for x in keys)
|
|
48
|
+
_handlers_re = re.compile(f'^\\s*({keys_str})(?:\\s+|;|$)', flags=re.I)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_handler(sql: Union[str, bytes]) -> Optional[Type[SQLHandler]]:
|
|
52
|
+
"""
|
|
53
|
+
Return a fusion handler for the given query.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
sql : str or bytes
|
|
58
|
+
The SQL query
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
SQLHandler - if a matching one exists
|
|
63
|
+
None - if no matching handler could be found
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
if not get_option('fusion.enabled'):
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
if isinstance(sql, (bytes, bytearray)):
|
|
70
|
+
sql = sql.decode('utf-8')
|
|
71
|
+
|
|
72
|
+
if _handlers_re is None:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
m = _handlers_re.match(sql)
|
|
76
|
+
if m:
|
|
77
|
+
return _handlers[re.sub(r'\s+', r' ', m.group(1).strip().upper())]
|
|
78
|
+
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def execute(
|
|
83
|
+
connection: connection.Connection,
|
|
84
|
+
sql: str,
|
|
85
|
+
handler: Optional[Type[SQLHandler]] = None,
|
|
86
|
+
) -> result.FusionSQLResult:
|
|
87
|
+
"""
|
|
88
|
+
Execute a SQL query in the management interface.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
connection : Connection
|
|
93
|
+
The SingleStoreDB connection object
|
|
94
|
+
sql : str
|
|
95
|
+
The SQL query
|
|
96
|
+
handler : SQLHandler, optional
|
|
97
|
+
The handler to use for the commands. If not supplied, one will be
|
|
98
|
+
looked up in the registry.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
FusionSQLResult
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
if not get_option('fusion.enabled'):
|
|
106
|
+
raise RuntimeError('management API queries have not been enabled')
|
|
107
|
+
|
|
108
|
+
if handler is None:
|
|
109
|
+
handler = get_handler(sql)
|
|
110
|
+
if handler is None:
|
|
111
|
+
raise RuntimeError(f'could not find handler for query: {sql}')
|
|
112
|
+
|
|
113
|
+
return handler(connection).execute(sql)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ShowFusionCommandsHandler(SQLHandler):
|
|
117
|
+
"""
|
|
118
|
+
SHOW FUSION COMMANDS [ like ];
|
|
119
|
+
|
|
120
|
+
# LIKE pattern
|
|
121
|
+
like = LIKE '<pattern>'
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def run(self, params: Dict[str, Any]) -> Optional[result.FusionSQLResult]:
|
|
126
|
+
res = result.FusionSQLResult()
|
|
127
|
+
res.add_field('Command', result.STRING)
|
|
128
|
+
|
|
129
|
+
data: List[Tuple[Any, ...]] = []
|
|
130
|
+
for _, v in sorted(_handlers.items()):
|
|
131
|
+
data.append((v.help.lstrip(),))
|
|
132
|
+
|
|
133
|
+
res.set_rows(data)
|
|
134
|
+
|
|
135
|
+
if params['like']:
|
|
136
|
+
res = res.like(Command=params['like'])
|
|
137
|
+
|
|
138
|
+
return res
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
ShowFusionCommandsHandler.register()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ShowFusionGrammarHandler(SQLHandler):
|
|
145
|
+
"""
|
|
146
|
+
SHOW FUSION GRAMMAR for_query;
|
|
147
|
+
|
|
148
|
+
# Query to show grammar for
|
|
149
|
+
for_query = FOR '<query>'
|
|
150
|
+
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def run(self, params: Dict[str, Any]) -> Optional[result.FusionSQLResult]:
|
|
154
|
+
res = result.FusionSQLResult()
|
|
155
|
+
res.add_field('Grammar', result.STRING)
|
|
156
|
+
handler = get_handler(params['for_query'])
|
|
157
|
+
data: List[Tuple[Any, ...]] = []
|
|
158
|
+
if handler is not None:
|
|
159
|
+
data.append((handler._grammar,))
|
|
160
|
+
res.set_rows(data)
|
|
161
|
+
return res
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
ShowFusionGrammarHandler.register()
|