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.

Files changed (120) hide show
  1. singlestoredb/__init__.py +33 -1
  2. singlestoredb/alchemy/__init__.py +90 -0
  3. singlestoredb/auth.py +5 -1
  4. singlestoredb/config.py +116 -14
  5. singlestoredb/connection.py +483 -516
  6. singlestoredb/converters.py +238 -135
  7. singlestoredb/exceptions.py +30 -2
  8. singlestoredb/functions/__init__.py +1 -0
  9. singlestoredb/functions/decorator.py +142 -0
  10. singlestoredb/functions/dtypes.py +1639 -0
  11. singlestoredb/functions/ext/__init__.py +2 -0
  12. singlestoredb/functions/ext/arrow.py +375 -0
  13. singlestoredb/functions/ext/asgi.py +661 -0
  14. singlestoredb/functions/ext/json.py +427 -0
  15. singlestoredb/functions/ext/mmap.py +306 -0
  16. singlestoredb/functions/ext/rowdat_1.py +744 -0
  17. singlestoredb/functions/signature.py +673 -0
  18. singlestoredb/fusion/__init__.py +11 -0
  19. singlestoredb/fusion/graphql.py +213 -0
  20. singlestoredb/fusion/handler.py +621 -0
  21. singlestoredb/fusion/handlers/stage.py +257 -0
  22. singlestoredb/fusion/handlers/utils.py +162 -0
  23. singlestoredb/fusion/handlers/workspace.py +412 -0
  24. singlestoredb/fusion/registry.py +164 -0
  25. singlestoredb/fusion/result.py +399 -0
  26. singlestoredb/http/__init__.py +27 -0
  27. singlestoredb/{http.py → http/connection.py} +555 -154
  28. singlestoredb/management/__init__.py +3 -0
  29. singlestoredb/management/billing_usage.py +148 -0
  30. singlestoredb/management/cluster.py +14 -6
  31. singlestoredb/management/manager.py +100 -38
  32. singlestoredb/management/organization.py +188 -0
  33. singlestoredb/management/region.py +5 -5
  34. singlestoredb/management/utils.py +281 -2
  35. singlestoredb/management/workspace.py +1344 -49
  36. singlestoredb/{clients/pymysqlsv → mysql}/__init__.py +16 -21
  37. singlestoredb/{clients/pymysqlsv → mysql}/_auth.py +39 -8
  38. singlestoredb/{clients/pymysqlsv → mysql}/charset.py +26 -23
  39. singlestoredb/{clients/pymysqlsv/connections.py → mysql/connection.py} +532 -165
  40. singlestoredb/{clients/pymysqlsv → mysql}/constants/CLIENT.py +0 -1
  41. singlestoredb/{clients/pymysqlsv → mysql}/constants/COMMAND.py +0 -1
  42. singlestoredb/{clients/pymysqlsv → mysql}/constants/CR.py +0 -2
  43. singlestoredb/{clients/pymysqlsv → mysql}/constants/ER.py +0 -1
  44. singlestoredb/{clients/pymysqlsv → mysql}/constants/FIELD_TYPE.py +1 -1
  45. singlestoredb/{clients/pymysqlsv → mysql}/constants/FLAG.py +0 -1
  46. singlestoredb/{clients/pymysqlsv → mysql}/constants/SERVER_STATUS.py +0 -1
  47. singlestoredb/mysql/converters.py +271 -0
  48. singlestoredb/{clients/pymysqlsv → mysql}/cursors.py +228 -112
  49. singlestoredb/mysql/err.py +92 -0
  50. singlestoredb/{clients/pymysqlsv → mysql}/optionfile.py +5 -4
  51. singlestoredb/{clients/pymysqlsv → mysql}/protocol.py +49 -20
  52. singlestoredb/mysql/tests/__init__.py +19 -0
  53. singlestoredb/{clients/pymysqlsv → mysql}/tests/base.py +32 -12
  54. singlestoredb/mysql/tests/conftest.py +37 -0
  55. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_DictCursor.py +11 -7
  56. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_SSCursor.py +17 -12
  57. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_basic.py +32 -24
  58. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_connection.py +130 -119
  59. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_converters.py +9 -7
  60. singlestoredb/mysql/tests/test_cursor.py +141 -0
  61. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_err.py +3 -2
  62. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_issues.py +35 -27
  63. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_load_local.py +13 -11
  64. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_nextset.py +7 -3
  65. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_optionfile.py +2 -1
  66. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/__init__.py +1 -1
  67. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
  68. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/capabilities.py +19 -17
  69. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/dbapi20.py +31 -22
  70. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +3 -4
  71. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +24 -20
  72. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +4 -4
  73. singlestoredb/{clients/pymysqlsv → mysql}/times.py +3 -4
  74. singlestoredb/pytest.py +283 -0
  75. singlestoredb/tests/empty.sql +0 -0
  76. singlestoredb/tests/ext_funcs/__init__.py +385 -0
  77. singlestoredb/tests/test.sql +210 -0
  78. singlestoredb/tests/test2.sql +1 -0
  79. singlestoredb/tests/test_basics.py +482 -115
  80. singlestoredb/tests/test_config.py +13 -13
  81. singlestoredb/tests/test_connection.py +241 -305
  82. singlestoredb/tests/test_dbapi.py +27 -0
  83. singlestoredb/tests/test_ext_func.py +1193 -0
  84. singlestoredb/tests/test_ext_func_data.py +1101 -0
  85. singlestoredb/tests/test_fusion.py +465 -0
  86. singlestoredb/tests/test_http.py +32 -26
  87. singlestoredb/tests/test_management.py +588 -8
  88. singlestoredb/tests/test_plugin.py +33 -0
  89. singlestoredb/tests/test_results.py +11 -12
  90. singlestoredb/tests/test_udf.py +687 -0
  91. singlestoredb/tests/utils.py +3 -2
  92. singlestoredb/utils/config.py +58 -0
  93. singlestoredb/utils/debug.py +13 -0
  94. singlestoredb/utils/mogrify.py +151 -0
  95. singlestoredb/utils/results.py +4 -1
  96. singlestoredb-1.0.4.dist-info/METADATA +139 -0
  97. singlestoredb-1.0.4.dist-info/RECORD +112 -0
  98. {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/WHEEL +1 -1
  99. singlestoredb-1.0.4.dist-info/entry_points.txt +2 -0
  100. singlestoredb/clients/pymysqlsv/converters.py +0 -365
  101. singlestoredb/clients/pymysqlsv/err.py +0 -144
  102. singlestoredb/clients/pymysqlsv/tests/__init__.py +0 -19
  103. singlestoredb/clients/pymysqlsv/tests/test_cursor.py +0 -133
  104. singlestoredb/clients/pymysqlsv/tests/thirdparty/test_MySQLdb/__init__.py +0 -9
  105. singlestoredb/drivers/__init__.py +0 -45
  106. singlestoredb/drivers/base.py +0 -198
  107. singlestoredb/drivers/cymysql.py +0 -38
  108. singlestoredb/drivers/http.py +0 -47
  109. singlestoredb/drivers/mariadb.py +0 -40
  110. singlestoredb/drivers/mysqlconnector.py +0 -49
  111. singlestoredb/drivers/mysqldb.py +0 -60
  112. singlestoredb/drivers/pymysql.py +0 -37
  113. singlestoredb/drivers/pymysqlsv.py +0 -35
  114. singlestoredb/drivers/pyodbc.py +0 -65
  115. singlestoredb-0.4.0.dist-info/METADATA +0 -111
  116. singlestoredb-0.4.0.dist-info/RECORD +0 -86
  117. /singlestoredb/{clients → fusion/handlers}/__init__.py +0 -0
  118. /singlestoredb/{clients/pymysqlsv → mysql}/constants/__init__.py +0 -0
  119. {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/LICENSE +0 -0
  120. {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()